多数据源动态优雅切换
核心思想
- 集成 AbstractRoutingDataSource 并重写 determineCurrentLookupKey 方法。determineCurrentLookupKey 方法的返回值决定了spring使用那个数据源,如果返回null则使用默认数据源。
- 在AbstractRoutingDataSource 的构造函数中完成 多数据源和默认数据源的注册。
- 定义 DruidConfig配置,该配置文件主要将三个 数据源注册到spring中,并且将 主数据员和第二个数据源通过AbstractRoutingDataSource 的实现注册到spring中。
核心代码
-
pom
<!-- Druid 数据连接池依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency> <!--MyBatis整合SpringBoot框架的起步依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- jdbc --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>2.7.8</version> </dependency> <!-- Mysql驱动包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.27</version> </dependency>
-
yml配置
# 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # 主库数据源 master: url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useUnicode=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8 username: root password: 123456 # 从库数据源 slave: # 从数据源开关/默认关闭 enabled: true url: jdbc:mysql://43.143.217.124:3306/hongbei?characterEncoding=utf8&useUnicode=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8 username: root password: 123456 # 初始连接数 initialSize: 5 # 最小连接池数量 minIdle: 10 # 最大连接池数量 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置检测连接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 设置白名单,不填则允许所有访问 allow: url-pattern: /druid/* # 控制台管理用户名和密码 login-username: ruoyi login-password: 123456 filter: stat: enabled: true # 慢SQL记录 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
-
前置工作1 定义数据库类型(属于规范)
/** * @Classname DataSourceType * @Description DataSourceType * @Date 2023-02-27 15:44 * @Created by lihw */ public enum DataSourceType { /** * 主库 */ MASTER, /** * 从库 */ SLAVE }
-
druid属性绑定,以及工具方法
import com.alibaba.druid.pool.DruidDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; /** * @Classname DruidProperties * @Description DruidProperties * @Date 2023-02-27 15:28 * @Created by lihw */ @Configuration public class DruidProperties { @Value("${spring.datasource.druid.initialSize}") private int initialSize; @Value("${spring.datasource.druid.minIdle}") private int minIdle; @Value("${spring.datasource.druid.maxActive}") private int maxActive; @Value("${spring.datasource.druid.maxWait}") private int maxWait; @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") private int timeBetweenEvictionRunsMillis; @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") private int minEvictableIdleTimeMillis; @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") private int maxEvictableIdleTimeMillis; @Value("${spring.datasource.druid.validationQuery}") private String validationQuery; @Value("${spring.datasource.druid.testWhileIdle}") private boolean testWhileIdle; @Value("${spring.datasource.druid.testOnBorrow}") private boolean testOnBorrow; @Value("${spring.datasource.druid.testOnReturn}") private boolean testOnReturn; public DruidDataSource dataSource(DruidDataSource datasource) { /** 配置初始化大小、最小、最大 */ datasource.setInitialSize(initialSize); datasource.setMaxActive(maxActive); datasource.setMinIdle(minIdle); /** 配置获取连接等待超时的时间 */ datasource.setMaxWait(maxWait); /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); /** * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 */ datasource.setValidationQuery(validationQuery); /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ datasource.setTestWhileIdle(testWhileIdle); /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ datasource.setTestOnBorrow(testOnBorrow); /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ datasource.setTestOnReturn(testOnReturn); return datasource; } }
-
定义动态数据源 以及 动态数据源 控制器
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import javax.sql.DataSource; import java.util.Map; /** * @Classname DynamicDataSource * @Description DynamicDataSource * @Date 2023-02-27 15:39 * @Created by lihw */ public class DynamicDataSource extends AbstractRoutingDataSource { /** * 数据源初始化操作,把多个数据源以及默认数据源注册到spring中 * @param defaultTargetDataSource * @param targetDataSources */ public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources){ super.setDefaultTargetDataSource(defaultTargetDataSource); super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } /** * 返回值决定使用哪个数据源,如果返回 null 则使用默认数据源 * @return */ @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceType(); } }
-
数据源控制器
import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @Classname DynamicDataSourceContextHolder * @Description DynamicDataSourceContextHolder * @Date 2023-02-27 15:39 * @Created by lihw */ public class DynamicDataSourceContextHolder { public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); /** * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 */ private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); /** * 设置数据源的变量 */ public static void setDataSourceType(String dsType) { log.info("切换到{}数据源", dsType); CONTEXT_HOLDER.set(dsType); } /** * 获得数据源的变量 */ public static String getDataSourceType() { return CONTEXT_HOLDER.get(); } /** * 清空数据源变量 */ public static void clearDataSourceType() { CONTEXT_HOLDER.remove(); } }
-
把多个数据源注册到动态数据源中, 并标记动态数据源为主数据源
import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; 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; /** * @Classname DruidConfig * @Description DruidConfig * @Date 2023-02-27 15:31 * @Created by lihw */ @Configuration public class DruidConfig implements BeanFactoryPostProcessor { /** Spring应用上下文环境,用于管理 注册到 spring中所有的bean */ private static ConfigurableListableBeanFactory beanFactory; /** * 注册主数据源 * @param druidProperties * @return */ @Bean @ConfigurationProperties("spring.datasource.druid.master") public DataSource masterDataSource(DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); } /** * 根据条件 : 注册 第二个数据源 * @param druidProperties * @return */ @Bean @ConfigurationProperties("spring.datasource.druid.slave") @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") public DataSource slaveDataSource(DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); } /** * 把 注册的多个数据源, 统一加入到动态数据源中,并把动态数据源注册到spring中,以便动态切换 * @param masterDataSource * @return */ @Bean("dynamicDataSource") // 标记动态数据源为主数据源,在相同的条件下,优先使用 此数据源 @Primary public DynamicDataSource dataSource(DataSource masterDataSource){ Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource"); return new DynamicDataSource(masterDataSource, targetDataSources); } /** * 设置数据源 * * @param targetDataSources 备选数据源集合 * @param sourceName 数据源名称 * @param beanName bean名称 */ public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) { try { DataSource dataSource = (DataSource) beanFactory.getBean(beanName); targetDataSources.put(sourceName, dataSource); } catch (Exception e) { } } /** * 重写BeanFactoryPostProcessor 的方法,用户接收 bean 工厂回调,bean 工厂中存了所有spring中注册的bean * @param configurableListableBeanFactory the bean factory used by the application context * @throws BeansException */ @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { beanFactory = configurableListableBeanFactory; } }
-
测试代码
@RequestMapping("/test") public class TestController { @Autowired StudentMapper studentMapper; @Autowired SysUserMapper sysUserMapper; @GetMapping("list") public List<Student> getUserList() { List<Student> userList = studentMapper.getUserList(); System.out.println(userList); return userList; } @GetMapping("msg2") public String getmsg2() { // 手动切换 数据源 DynamicDataSourceContextHolder.setDataSourceType("SLAVE"); List<SysUser> user = sysUserMapper.getUserList(); DynamicDataSourceContextHolder.clearDataSourceType(); System.out.println(user); return "msg22"; } }
测试结果
2023-02-27 16:25:57.770 INFO 6988 --- [nio-8080-exec-1] com.alibaba.druid.pool.DruidDataSource :** {dataSource-1} inited
[Student(id=1612374211110744066, name=小红1, age=20, classId=1, teacherId=1, createTime=null, updateTime=null), Student(id=1612374211354013699, name=小红, age=20, classId=12, teacherId=21, createTime=null, updateTime=null), Student(id=1625387649604820994, name=小王, age=30, classId=null, teacherId=null, createTime=null, updateTime=null), Student(id=1625387978916392961, name=小王, age=30, classId=null, teacherId=null, createTime=null, updateTime=null), Student(id=1625389122829021186, name=小王, age=30, classId=null, teacherId=null, createTime=null, updateTime=null), Student(id=1625389706172817410, name=小王, age=30, classId=null, teacherId=null, createTime=null, updateTime=null), Student(id=1625389938096738305, name=小王, age=30, classId=null, teacherId=null, createTime=Tue Feb 14 15:02:14 CST 2023, updateTime=null)]**
2023-02-27 16:25:59.710 INFO 6988 --- [nio-8080-exec-3] c.e.d.d.DynamicDataSourceContextHolder : 切换到SLAVE数据源
2023-02-27 16:26:00.757 INFO 6988 --- [nio-8080-exec-3] com.alibaba.druid.pool.DruidDataSource : **{dataSource-2} inited
[SysUser(userId=1, userName=admin, nickName=若依), SysUser(userId=2, userName=ry, nickName=若依), SysUser(userId=100, userName=lhw, nickName=李洪伟)]**
利用spring 注解优雅切换
-
spring aop拦截器
<!-- SpringBoot 拦截器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.7.9</version> </dependency>
-
定义注解
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import com.ruoyi.common.enums.DataSourceType; /** * 自定义多数据源切换注解 * * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 * * @author ruoyi */ @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface DataSource { /** * 切换数据源名称 */ public DataSourceType value() default DataSourceType.MASTER; }
-
aop 切面
import java.util.Objects; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; /** * 多数据源处理 * * @author ruoyi */ @Aspect @Order(1) @Component public class DataSourceAspect { protected Logger logger = LoggerFactory.getLogger(getClass()); @Pointcut("@annotation(com.example.demo.druid2.DataSource)" + "|| @within(com.example.demo.druid2.DataSource)") public void dsPointCut() { } @Around("dsPointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { DataSource dataSource = getDataSource(point); if (dataSource != null) { DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); } try { return point.proceed(); } finally { // 销毁数据源 在执行方法之后 DynamicDataSourceContextHolder.clearDataSourceType(); } } /** * 获取需要切换的数据源 */ public DataSource getDataSource(ProceedingJoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); if (Objects.nonNull(dataSource)) { return dataSource; } return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); } }