You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

README.md 18 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. # spring-boot-demo-dynamic-datasource
  2. > 此 demo 主要演示了 Spring Boot 项目如何通过接口`动态添加/删除`数据源,添加数据源之后如何`动态切换`数据源,然后使用 mybatis 查询切换后的数据源的数据。
  3. ## 1. 环境准备
  4. 1. 执行 db 目录下的SQL脚本
  5. 2. 在默认数据源下执行 `init.sql`
  6. 3. 在所有数据源分别执行 `user.sql`
  7. ## 2. 主要代码
  8. ### 2.1.pom.xml
  9. ```xml
  10. <?xml version="1.0" encoding="UTF-8"?>
  11. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  12. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  13. <modelVersion>4.0.0</modelVersion>
  14. <artifactId>spring-boot-demo-dynamic-datasource</artifactId>
  15. <version>1.0.0-SNAPSHOT</version>
  16. <packaging>jar</packaging>
  17. <name>spring-boot-demo-dynamic-datasource</name>
  18. <description>Demo project for Spring Boot</description>
  19. <parent>
  20. <groupId>com.xkcoding</groupId>
  21. <artifactId>spring-boot-demo</artifactId>
  22. <version>1.0.0-SNAPSHOT</version>
  23. </parent>
  24. <properties>
  25. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  26. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  27. <java.version>1.8</java.version>
  28. </properties>
  29. <dependencies>
  30. <dependency>
  31. <groupId>org.springframework.boot</groupId>
  32. <artifactId>spring-boot-starter-web</artifactId>
  33. </dependency>
  34. <dependency>
  35. <groupId>org.springframework.boot</groupId>
  36. <artifactId>spring-boot-starter-aop</artifactId>
  37. </dependency>
  38. <dependency>
  39. <groupId>tk.mybatis</groupId>
  40. <artifactId>mapper-spring-boot-starter</artifactId>
  41. <version>2.1.5</version>
  42. </dependency>
  43. <dependency>
  44. <groupId>mysql</groupId>
  45. <artifactId>mysql-connector-java</artifactId>
  46. <scope>runtime</scope>
  47. </dependency>
  48. <dependency>
  49. <groupId>org.projectlombok</groupId>
  50. <artifactId>lombok</artifactId>
  51. <optional>true</optional>
  52. </dependency>
  53. <dependency>
  54. <groupId>org.springframework.boot</groupId>
  55. <artifactId>spring-boot-starter-test</artifactId>
  56. <scope>test</scope>
  57. </dependency>
  58. </dependencies>
  59. <build>
  60. <finalName>spring-boot-demo-dynamic-datasource</finalName>
  61. <plugins>
  62. <plugin>
  63. <groupId>org.springframework.boot</groupId>
  64. <artifactId>spring-boot-maven-plugin</artifactId>
  65. </plugin>
  66. </plugins>
  67. </build>
  68. </project>
  69. ```
  70. ### 2.2. 基础配置类
  71. - DatasourceConfiguration.java
  72. > 这个类主要是通过 `DataSourceBuilder` 去构建一个我们自定义的数据源,将其放入 Spring 容器里
  73. ```java
  74. /**
  75. * <p>
  76. * 数据源配置
  77. * </p>
  78. *
  79. * @author yangkai.shen
  80. * @date Created in 2019-09-04 10:27
  81. */
  82. @Configuration
  83. public class DatasourceConfiguration {
  84. @Bean
  85. @ConfigurationProperties(prefix = "spring.datasource")
  86. public DataSource dataSource() {
  87. DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
  88. dataSourceBuilder.type(DynamicDataSource.class);
  89. return dataSourceBuilder.build();
  90. }
  91. }
  92. ```
  93. - MybatisConfiguration.java
  94. > 这个类主要是将我们上一步构建出来的数据源配置到 Mybatis 的 `SqlSessionFactory` 里
  95. ```java
  96. /**
  97. * <p>
  98. * mybatis配置
  99. * </p>
  100. *
  101. * @author yangkai.shen
  102. * @date Created in 2019-09-04 16:20
  103. */
  104. @Configuration
  105. @MapperScan(basePackages = "com.xkcoding.dynamicdatasource.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
  106. public class MybatisConfiguration {
  107. /**
  108. * 创建会话工厂。
  109. *
  110. * @param dataSource 数据源
  111. * @return 会话工厂
  112. */
  113. @Bean(name = "sqlSessionFactory")
  114. @SneakyThrows
  115. public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) {
  116. SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
  117. bean.setDataSource(dataSource);
  118. return bean.getObject();
  119. }
  120. }
  121. ```
  122. ### 2.3. 动态数据源主要逻辑
  123. - DatasourceConfigContextHolder.java
  124. > 该类主要用于绑定当前线程所使用的数据源 id,通过 ThreadLocal 保证同一线程内不可被修改
  125. ```java
  126. /**
  127. * <p>
  128. * 数据源标识管理
  129. * </p>
  130. *
  131. * @author yangkai.shen
  132. * @date Created in 2019-09-04 14:16
  133. */
  134. public class DatasourceConfigContextHolder {
  135. private static final ThreadLocal<Long> DATASOURCE_HOLDER = ThreadLocal.withInitial(() -> DatasourceHolder.DEFAULT_ID);
  136. /**
  137. * 设置默认数据源
  138. */
  139. public static void setDefaultDatasource() {
  140. DATASOURCE_HOLDER.remove();
  141. setCurrentDatasourceConfig(DatasourceHolder.DEFAULT_ID);
  142. }
  143. /**
  144. * 获取当前数据源配置id
  145. *
  146. * @return 数据源配置id
  147. */
  148. public static Long getCurrentDatasourceConfig() {
  149. return DATASOURCE_HOLDER.get();
  150. }
  151. /**
  152. * 设置当前数据源配置id
  153. *
  154. * @param id 数据源配置id
  155. */
  156. public static void setCurrentDatasourceConfig(Long id) {
  157. DATASOURCE_HOLDER.set(id);
  158. }
  159. }
  160. ```
  161. - DynamicDataSource.java
  162. > 该类继承 `com.zaxxer.hikari.HikariDataSource`,主要用于动态切换数据源连接。
  163. ```java
  164. /**
  165. * <p>
  166. * 动态数据源
  167. * </p>
  168. *
  169. * @author yangkai.shen
  170. * @date Created in 2019-09-04 10:41
  171. */
  172. @Slf4j
  173. public class DynamicDataSource extends HikariDataSource {
  174. @Override
  175. public Connection getConnection() throws SQLException {
  176. // 获取当前数据源 id
  177. Long id = DatasourceConfigContextHolder.getCurrentDatasourceConfig();
  178. // 根据当前id获取数据源
  179. HikariDataSource datasource = DatasourceHolder.INSTANCE.getDatasource(id);
  180. if (null == datasource) {
  181. datasource = initDatasource(id);
  182. }
  183. return datasource.getConnection();
  184. }
  185. /**
  186. * 初始化数据源
  187. * @param id 数据源id
  188. * @return 数据源
  189. */
  190. private HikariDataSource initDatasource(Long id) {
  191. HikariDataSource dataSource = new HikariDataSource();
  192. // 判断是否是默认数据源
  193. if (DatasourceHolder.DEFAULT_ID.equals(id)) {
  194. // 默认数据源根据 application.yml 配置的生成
  195. DataSourceProperties properties = SpringUtil.getBean(DataSourceProperties.class);
  196. dataSource.setJdbcUrl(properties.getUrl());
  197. dataSource.setUsername(properties.getUsername());
  198. dataSource.setPassword(properties.getPassword());
  199. dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
  200. } else {
  201. // 不是默认数据源,通过缓存获取对应id的数据源的配置
  202. DatasourceConfig datasourceConfig = DatasourceConfigCache.INSTANCE.getConfig(id);
  203. if (datasourceConfig == null) {
  204. throw new RuntimeException("无此数据源");
  205. }
  206. dataSource.setJdbcUrl(datasourceConfig.buildJdbcUrl());
  207. dataSource.setUsername(datasourceConfig.getUsername());
  208. dataSource.setPassword(datasourceConfig.getPassword());
  209. dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
  210. }
  211. // 将创建的数据源添加到数据源管理器中,绑定当前线程
  212. DatasourceHolder.INSTANCE.addDatasource(id, dataSource);
  213. return dataSource;
  214. }
  215. }
  216. ```
  217. - DatasourceScheduler.java
  218. > 该类主要用于调度任务
  219. ```java
  220. /**
  221. * <p>
  222. * 数据源缓存释放调度器
  223. * </p>
  224. *
  225. * @author yangkai.shen
  226. * @date Created in 2019-09-04 14:42
  227. */
  228. public enum DatasourceScheduler {
  229. /**
  230. * 当前实例
  231. */
  232. INSTANCE;
  233. private AtomicInteger cacheTaskNumber = new AtomicInteger(1);
  234. private ScheduledExecutorService scheduler;
  235. DatasourceScheduler() {
  236. create();
  237. }
  238. private void create() {
  239. this.shutdown();
  240. this.scheduler = new ScheduledThreadPoolExecutor(10, r -> new Thread(r, String.format("Datasource-Release-Task-%s", cacheTaskNumber.getAndIncrement())));
  241. }
  242. private void shutdown() {
  243. if (null != this.scheduler) {
  244. this.scheduler.shutdown();
  245. }
  246. }
  247. public void schedule(Runnable task,long delay){
  248. this.scheduler.scheduleAtFixedRate(task, delay, delay, TimeUnit.MILLISECONDS);
  249. }
  250. }
  251. ```
  252. - DatasourceManager.java
  253. > 该类主要用于管理数据源,记录数据源最后使用时间,同时判断是否长时间未使用,超过一定时间未使用,会被释放连接
  254. ```java
  255. /**
  256. * <p>
  257. * 数据源管理类
  258. * </p>
  259. *
  260. * @author yangkai.shen
  261. * @date Created in 2019-09-04 14:27
  262. */
  263. public class DatasourceManager {
  264. /**
  265. * 默认释放时间
  266. */
  267. private static final Long DEFAULT_RELEASE = 10L;
  268. /**
  269. * 数据源
  270. */
  271. @Getter
  272. private HikariDataSource dataSource;
  273. /**
  274. * 上一次使用时间
  275. */
  276. private LocalDateTime lastUseTime;
  277. public DatasourceManager(HikariDataSource dataSource) {
  278. this.dataSource = dataSource;
  279. this.lastUseTime = LocalDateTime.now();
  280. }
  281. /**
  282. * 是否已过期,如果过期则关闭数据源
  283. *
  284. * @return 是否过期,{@code true} 过期,{@code false} 未过期
  285. */
  286. public boolean isExpired() {
  287. if (LocalDateTime.now().isBefore(this.lastUseTime.plusMinutes(DEFAULT_RELEASE))) {
  288. return false;
  289. }
  290. this.dataSource.close();
  291. return true;
  292. }
  293. /**
  294. * 刷新上次使用时间
  295. */
  296. public void refreshTime() {
  297. this.lastUseTime = LocalDateTime.now();
  298. }
  299. }
  300. ```
  301. - DatasourceHolder.java
  302. > 该类主要用于管理数据源,同时通过 `DatasourceScheduler` 定时检查数据源是否长时间未使用,超时则释放连接
  303. ```java
  304. /**
  305. * <p>
  306. * 数据源管理
  307. * </p>
  308. *
  309. * @author yangkai.shen
  310. * @date Created in 2019-09-04 14:23
  311. */
  312. public enum DatasourceHolder {
  313. /**
  314. * 当前实例
  315. */
  316. INSTANCE;
  317. /**
  318. * 启动执行,定时5分钟清理一次
  319. */
  320. DatasourceHolder() {
  321. DatasourceScheduler.INSTANCE.schedule(this::clearExpiredDatasource, 5 * 60 * 1000);
  322. }
  323. /**
  324. * 默认数据源的id
  325. */
  326. public static final Long DEFAULT_ID = -1L;
  327. /**
  328. * 管理动态数据源列表。
  329. */
  330. private static final Map<Long, DatasourceManager> DATASOURCE_CACHE = new ConcurrentHashMap<>();
  331. /**
  332. * 添加动态数据源
  333. *
  334. * @param id 数据源id
  335. * @param dataSource 数据源
  336. */
  337. public synchronized void addDatasource(Long id, HikariDataSource dataSource) {
  338. DatasourceManager datasourceManager = new DatasourceManager(dataSource);
  339. DATASOURCE_CACHE.put(id, datasourceManager);
  340. }
  341. /**
  342. * 查询动态数据源
  343. *
  344. * @param id 数据源id
  345. * @return 数据源
  346. */
  347. public synchronized HikariDataSource getDatasource(Long id) {
  348. if (DATASOURCE_CACHE.containsKey(id)) {
  349. DatasourceManager datasourceManager = DATASOURCE_CACHE.get(id);
  350. datasourceManager.refreshTime();
  351. return datasourceManager.getDataSource();
  352. }
  353. return null;
  354. }
  355. /**
  356. * 清除超时的数据源
  357. */
  358. public synchronized void clearExpiredDatasource() {
  359. DATASOURCE_CACHE.forEach((k, v) -> {
  360. // 排除默认数据源
  361. if (!DEFAULT_ID.equals(k)) {
  362. if (v.isExpired()) {
  363. DATASOURCE_CACHE.remove(k);
  364. }
  365. }
  366. });
  367. }
  368. /**
  369. * 清除动态数据源
  370. * @param id 数据源id
  371. */
  372. public synchronized void removeDatasource(Long id) {
  373. if (DATASOURCE_CACHE.containsKey(id)) {
  374. // 关闭数据源
  375. DATASOURCE_CACHE.get(id).getDataSource().close();
  376. // 移除缓存
  377. DATASOURCE_CACHE.remove(id);
  378. }
  379. }
  380. }
  381. ```
  382. - DatasourceConfigCache.java
  383. > 该类主要用于缓存数据源的配置,用户生成数据源时,获取数据源连接参数
  384. ```java
  385. /**
  386. * <p>
  387. * 数据源配置缓存
  388. * </p>
  389. *
  390. * @author yangkai.shen
  391. * @date Created in 2019-09-04 17:13
  392. */
  393. public enum DatasourceConfigCache {
  394. /**
  395. * 当前实例
  396. */
  397. INSTANCE;
  398. /**
  399. * 管理动态数据源列表。
  400. */
  401. private static final Map<Long, DatasourceConfig> CONFIG_CACHE = new ConcurrentHashMap<>();
  402. /**
  403. * 添加数据源配置
  404. *
  405. * @param id 数据源配置id
  406. * @param config 数据源配置
  407. */
  408. public synchronized void addConfig(Long id, DatasourceConfig config) {
  409. CONFIG_CACHE.put(id, config);
  410. }
  411. /**
  412. * 查询数据源配置
  413. *
  414. * @param id 数据源配置id
  415. * @return 数据源配置
  416. */
  417. public synchronized DatasourceConfig getConfig(Long id) {
  418. if (CONFIG_CACHE.containsKey(id)) {
  419. return CONFIG_CACHE.get(id);
  420. }
  421. return null;
  422. }
  423. /**
  424. * 清除数据源配置
  425. */
  426. public synchronized void removeConfig(Long id) {
  427. CONFIG_CACHE.remove(id);
  428. // 同步清除 DatasourceHolder 对应的数据源
  429. DatasourceHolder.INSTANCE.removeDatasource(id);
  430. }
  431. }
  432. ```
  433. ### 2.4. 启动类
  434. > 启动后,使用默认数据源查询数据源配置列表,将其缓存到 `DatasourceConfigCache` 里,以供后续使用
  435. ```java
  436. /**
  437. * <p>
  438. * 启动器
  439. * </p>
  440. *
  441. * @author yangkai.shen
  442. * @date Created in 2019-09-04 17:57
  443. */
  444. @SpringBootApplication
  445. @RequiredArgsConstructor(onConstructor_ = @Autowired)
  446. public class SpringBootDemoDynamicDatasourceApplication implements CommandLineRunner {
  447. private final DatasourceConfigMapper configMapper;
  448. public static void main(String[] args) {
  449. SpringApplication.run(SpringBootDemoDynamicDatasourceApplication.class, args);
  450. }
  451. @Override
  452. public void run(String... args) {
  453. // 设置默认的数据源
  454. DatasourceConfigContextHolder.setDefaultDatasource();
  455. // 查询所有数据库配置列表
  456. List<DatasourceConfig> datasourceConfigs = configMapper.selectAll();
  457. System.out.println("加载其余数据源配置列表: " + datasourceConfigs);
  458. // 将数据库配置加入缓存
  459. datasourceConfigs.forEach(config -> DatasourceConfigCache.INSTANCE.addConfig(config.getId(), config));
  460. }
  461. }
  462. ```
  463. ### 2.5. 其余代码参考 demo
  464. ## 3. 测试
  465. 启动项目,可以看到控制台读取到数据库已配置的数据源信息
  466. ![image-20190905164824155](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062351.png)
  467. 通过 PostMan 等工具测试
  468. - 默认数据源查询
  469. ![image-20190905165240373](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062353.png)
  470. - 根据数据源id为1的数据源查询
  471. ![image-20190905165323097](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062354.png)
  472. - 根据数据源id为2的数据源查询
  473. ![image-20190905165350355](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062355.png)
  474. - 可以通过测试数据源的`增加/删除`,再去查询对应数据源的数据
  475. > 删除数据源:
  476. >
  477. > - DELETE http://localhost:8080/config/{id}
  478. >
  479. > 新增数据源:
  480. >
  481. > - POST http://localhost:8080/config
  482. >
  483. > - 参数:
  484. >
  485. > ```json
  486. > {
  487. > "host": "数据库IP",
  488. > "port": 3306,
  489. > "username": "用户名",
  490. > "password": "密码",
  491. > "database": "数据库"
  492. > }
  493. > ```
  494. ## 4. 优化
  495. 如上测试,我们只需要通过在 header 里传递数据源的参数,即可做到动态切换数据源,怎么做到的呢?
  496. 答案就是 `AOP`
  497. ```java
  498. /**
  499. * <p>
  500. * 数据源选择器切面
  501. * </p>
  502. *
  503. * @author yangkai.shen
  504. * @date Created in 2019-09-04 16:52
  505. */
  506. @Aspect
  507. @Component
  508. @RequiredArgsConstructor(onConstructor_ = @Autowired)
  509. public class DatasourceSelectorAspect {
  510. @Pointcut("execution(public * com.xkcoding.dynamic.datasource.controller.*.*(..))")
  511. public void datasourcePointcut() {
  512. }
  513. /**
  514. * 前置操作,拦截具体请求,获取header里的数据源id,设置线程变量里,用于后续切换数据源
  515. */
  516. @Before("datasourcePointcut()")
  517. public void doBefore(JoinPoint joinPoint) {
  518. Signature signature = joinPoint.getSignature();
  519. MethodSignature methodSignature = (MethodSignature) signature;
  520. Method method = methodSignature.getMethod();
  521. // 排除不可切换数据源的方法
  522. DefaultDatasource annotation = method.getAnnotation(DefaultDatasource.class);
  523. if (null != annotation) {
  524. DatasourceConfigContextHolder.setDefaultDatasource();
  525. } else {
  526. RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
  527. ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
  528. HttpServletRequest request = attributes.getRequest();
  529. String configIdInHeader = request.getHeader("Datasource-Config-Id");
  530. if (StringUtils.hasText(configIdInHeader)) {
  531. long configId = Long.parseLong(configIdInHeader);
  532. DatasourceConfigContextHolder.setCurrentDatasourceConfig(configId);
  533. } else {
  534. DatasourceConfigContextHolder.setDefaultDatasource();
  535. }
  536. }
  537. }
  538. /**
  539. * 后置操作,设置回默认的数据源id
  540. */
  541. @AfterReturning("datasourcePointcut()")
  542. public void doAfter() {
  543. DatasourceConfigContextHolder.setDefaultDatasource();
  544. }
  545. }
  546. ```
  547. 此时需要考虑,我们是否每个方法都允许用户去切换数据源呢?答案肯定是不行的,所以我们定义了一个注解去标识,当前方法仅可以使用默认数据源。
  548. ```java
  549. /**
  550. * <p>
  551. * 用户标识仅可以使用默认数据源
  552. * </p>
  553. *
  554. * @author yangkai.shen
  555. * @date Created in 2019-09-04 17:37
  556. */
  557. @Target({ElementType.METHOD})
  558. @Retention(RetentionPolicy.RUNTIME)
  559. @Documented
  560. public @interface DefaultDatasource {
  561. }
  562. ```
  563. 完结,撒花✿✿ヽ(°▽°)ノ✿