# DynamicDatasource **Repository Path**: bruce6213/dynamic-datasource ## Basic Information - **Project Name**: DynamicDatasource - **Description**: SpringBoot 多数据源切换 Demo - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-04-17 - **Last Updated**: 2025-03-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 单数据源 SpringBoot默认会通过DataSourceAutoConfiguration类自动装配**单数据源**, 注意此处装配的是单数据源。 默认一定会走自动装配,即若读取不到相关数据库配置则报错。 ## 原理 DataSourceAutoConfiguration.class会自动查找application.yml或者properties文件里的spring.datasource.* 相关属性并自动配置单数据源。(注意相关属性一定要在同一级,**否则可能导致自动装配失败**) ``` spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: ``` 即**此方式不可能使用多数据源**,即使加上@Primary注解,程序本质上仍是单数据源,因为无法使用到另外的数据源。 ## 关闭SpringBoot的自动装配单数据源 ``` // 在相关配置类上添加如下注解 @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) ``` ## 可能的坑 若是存在”第四级“标签,则SpringBoot无法读取到相关属性值,导致遗留隐患。 ``` spring: # datasource 数据源配置内容 datasource: # 订单数据源配置 orders: url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.jdbc.Driver username: root password: # HikariCP 自定义配置,对应 HikariConfig 配置属性类 hikari: minimum-idle: 20 # 池中维护的最小空闲连接数,默认为 10 个。 maximum-pool-size: 20 # 池中最大连接数,包括闲置和使用中的连接,默认为 10 个。 # 用户数据源配置 users: url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.jdbc.Driver username: root password: # HikariCP 自定义配置,对应 HikariConfig 配置属性类 hikari: minimum-idle: 15 # 池中维护的最小空闲连接数,默认为 10 个。 maximum-pool-size: 15 # 池中最大连接数,包括闲置和使用中的连接,默认为 10 个。 ``` ### 解决思路 ``` @Configuration public class DataSourceConfig { /** * 创建 orders 数据源的配置对象 */ @Primary @Bean(name = "ordersDataSourceProperties") @ConfigurationProperties(prefix = "spring.datasource.orders") // 读取 spring.datasource.orders 配置到 DataSourceProperties 对象 public DataSourceProperties ordersDataSourceProperties() { return new DataSourceProperties(); } /** * 创建 orders 数据源 */ @Bean(name = "ordersDataSource") @ConfigurationProperties(prefix = "spring.datasource.orders.hikari") // 读取 spring.datasource.orders 配置到 HikariDataSource 对象 public DataSource ordersDataSource() { // <1.1> 获得 DataSourceProperties 对象 DataSourceProperties properties = this.ordersDataSourceProperties(); // <1.2> 创建 HikariDataSource 对象 return createHikariDataSource(properties); } /** * 创建 users 数据源的配置对象 */ @Bean(name = "usersDataSourceProperties") @ConfigurationProperties(prefix = "spring.datasource.users") // 读取 spring.datasource.users 配置到 DataSourceProperties 对象 public DataSourceProperties usersDataSourceProperties() { return new DataSourceProperties(); } /** * 创建 users 数据源 */ @Bean(name = "usersDataSource") @ConfigurationProperties(prefix = "spring.datasource.users.hikari") public DataSource usersDataSource() { // 获得 DataSourceProperties 对象 DataSourceProperties properties = this.usersDataSourceProperties(); // 创建 HikariDataSource 对象 return createHikariDataSource(properties); } private static HikariDataSource createHikariDataSource(DataSourceProperties properties) { // 创建 HikariDataSource 对象 HikariDataSource dataSource = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); // 设置线程池名 if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } return dataSource; } } ``` # 多数据源 ## 核心思想 ``` 一个请求是一个线程 线程局部变量、设置value AbstractRoutingDataSource 也通过线程局部变量拿到key,去map中取数据源 注意:禁用springboot的单数据源自动装配 ``` 自定义的包装类,让它继承DataSource;在这个自定义包装类**内部维护一个存储多个数据源的Map**,在根据业务逻辑传过来的key,动态获取对应的DataSource。 同时spring框架其实已经帮我们实现了这个包装类**org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource**, AbstractRoutingDataSource是一个抽象类,该类中determineTargetDataSource()方法将**调用determineCurrentLookupKey()方法来动态获取数据源**(自定义获取key的方式)。 ![img](https://gitee.com/bruce6213/image/raw/master/284c38fd98be40558cac4ad85c769919.png) ## 多数据源配置 ![image-20240207145131764](https://gitee.com/bruce6213/image/raw/master/image-20240207145131764.png) ``` import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * @Description : * @Author : Bruce Lee * @CreateTime : 2024/4/16 */ @Configuration @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) public class DataSourceConfig { @Bean(name = "master") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "slaver") @ConfigurationProperties(prefix = "spring.datasource.slaver") public DataSource slaverDataSource() { return DataSourceBuilder.create().build(); } // 生成动态数据源 @Bean @Primary public DataSource dataSource(){ Map dataSourceMap = new HashMap<>(2); dataSourceMap.put("master", masterDataSource()); dataSourceMap.put("slaver", slaverDataSource()); DynamicDatasource dynamicDatasource=new DynamicDatasource(); dynamicDatasource.setTargetDataSources(dataSourceMap); // 设置默认数据源 dynamicDatasource.setDefaultTargetDataSource(masterDataSource()); return dynamicDatasource; } } ``` ## 自定义包装类,继承DataSource **作用:**获取数据源所对应的key ``` import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * @Description : 动态数据源 * @Author : Bruce Lee * @CreateTime : 2024/4/16 */ public class DynamicDatasource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicDatasourceHolder.getDataSource(); } } ``` ## **自定义数据源key的获取方式** ``` /** * @Description : 用于存储动态数据源的key * @Author : Bruce Lee * @CreateTime : 2024/2/6 */ public class DynamicDatasourceHolder { /** * 至于如何通过key拿到数据源对象,SpringBoot内部已经封装好了 */ private static final ThreadLocal DATASOURCE_HOLDER = new ThreadLocal<>(); public static String getDataSource() { return DATASOURCE_HOLDER.get(); } public static void setDataSource(String dataSourceKey) { DATASOURCE_HOLDER.set(dataSourceKey); } public static void removeDataSource() { DATASOURCE_HOLDER.remove(); } } ``` ## 进阶一下 尽管现在已经实现了动态切换数据源,但每次主从数据库切换后需恢复默认,且数据库之间容易混淆。 ``` void contextLoads() { // 切换数据库 DynamicDatasourceHolder.setDataSource("slaver"); userMapper.insertUser("test", "123456"); // 切换为默认数据库 DynamicDatasourceHolder.removeDataSource(); } ``` ### 解决方案 AOP 切面代理,使数据源的切换及恢复与业务解耦合 ``` import com.example.datasource.annotation.MyDataSource; import com.example.datasource.config.DynamicDatasourceHolder; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; /** * @Description : 切面:切换数据源 * @Author : Bruce Lee * @CreateTime : 2024/4/16 */ @Aspect @Component @Slf4j public class DynamicDatasourceAspect { @Around(value = "@annotation(com.example.datasource.annotation.MyDataSource)") public void around(ProceedingJoinPoint joinPoint) throws Throwable { // // 获取类上的注解 // Class targetClass = joinPoint.getTarget().getClass(); // MyDataSource annotation = targetClass.getAnnotation(MyDataSource.class); // 获取方法上的注解 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); MyDataSource annotationMethod = methodSignature.getMethod().getAnnotation(MyDataSource.class); if (annotationMethod != null){ String dataSource = annotationMethod.value(); // 切换数据源 DynamicDatasourceHolder.setDataSource(dataSource); } // 执行方法 joinPoint.proceed(joinPoint.getArgs()); // 切换回默认数据源 DynamicDatasourceHolder.removeDataSource(); } } ``` # 遇到的问题 ## 单数据源与多数据源配置url格式问题 ### 问题描述 ![image-20240416161410412](https://gitee.com/bruce6213/image/raw/master/image-20240416161410412.png) ### 解决方案 #### 单数据源 ``` spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false username: root password: root ``` #### 多数据源 ``` spring: datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false username: root password: root slaver: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/test2?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false username: root password: root ``` # 参考文章 [多数据源切换原理](https://blog.csdn.net/weixin_42304484/article/details/124482994) [动态数据源切换 Demo](https://juejin.cn/post/7261601725840179255#heading-3) [多数据源切换影响事务管理](https://www.cnblogs.com/wtzbk/p/17157708.html)