多数据源动态优雅切换

核心思想
  1. 集成 AbstractRoutingDataSource 并重写 determineCurrentLookupKey 方法。determineCurrentLookupKey 方法的返回值决定了spring使用那个数据源,如果返回null则使用默认数据源。
  2. 在AbstractRoutingDataSource 的构造函数中完成 多数据源和默认数据源的注册。
  3. 定义 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);
        }
    }