美文网首页springweb
动态数据源配置

动态数据源配置

作者: 李木鱼789 | 来源:发表于2019-09-30 16:31 被阅读0次

    解决思路:使用spring提供的AbstractRoutingDataSource结合AOP进行动态配置,ThreadLocal进行动态数据存储。


    实现步骤:

    • 枚举类 DataSourceType:
      枚举多种数据源,与自定义注解配合使用
    • 自定义注解 DataSource:
      注解,配合AOP可进行无侵入的多数据源切换
    • 数据源切换处理 DynamicDataSourceContextHolder:
      维护了ThreadLocal对象,用于处理数据源切换
    • 多数据源配置 DynamicDataSource(核心):
      继承AbstractRoutingDataSource,Spring实现,详见解析
    • AOP切面 DataSourceAspect:
      配合注解实现无侵入的动态数据源切换
    • 多数据源配置 DruidConfiguration
      注入多数据源配置对象

    ps:详见源码


    使用方式

    //在需要更改数据源的方法上加
    @DataSource(value = DataSourceType.Slave)
    

    运行原理

    • 多数据源初始化

      • application.yml中配置多数据源
      spring:
          datasource:
              type: com.zaxxer.hikari.HikariDataSource
              driver-class-name: com.mysql.cj.jdbc.Driver
              master:
                  username: root
                  password: 150512
                  jdbc-url: jdbc:mysql://localhost:3306/cy?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
                  hikari:
                      minimum-idle: 5
                      maximum-pool-size: 15
                      auto-commit: true
                      idle-timeout: 30000
                      max-lifetime: 1800000
                      connection-timeout: 30000
              slave:
                  enabled: true
                  username: root
                  password: 150512
                  jdbc-url: jdbc:mysql://localhost:3306/yc?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
                  hikari:
                      minimum-idle: 5
                      maximum-pool-size: 15
                      auto-commit: true
                      idle-timeout: 30000
                      max-lifetime: 1800000
                  connection-timeout: 30000
      
      • 将数据源配置从配置文件中读出,放入targetDataSources这个map中,注入ioc容器
      @Bean(name = "dynamicDataSource")
      @Primary
      public DynamicDataSource dataSource(){
      
          Map<Object, Object> targetDataSources = new HashMap<>();
      
          targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource());
          targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource());
      
          return new DynamicDataSource(masterDataSource(), targetDataSources);
      }
      
      • 初始化动态数据源
      public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources){
      
          super.setDefaultTargetDataSource(defaultTargetDataSource);
      
          super.setTargetDataSources(targetDataSources);
      
          super.afterPropertiesSet();
      }
      
    • 在需要切换数据源的方法上添加注解

      @GetMapping("/testDs")
      @SwitchDataSource(value = DataSourceType.SLAVE)
      public Object testDs(){
      
          String sql="select id,username from user where id=?";
      
          RowMapper<User> rowMapper=new BeanPropertyRowMapper<>(User.class);
      
          User user = jdbcTemplate.queryForObject(sql, rowMapper,52);
      
          return user;
      }
      
    • 系统检测到注解,执行AOP方法,切换数据源

      @Pointcut(value = "@annotation(com.cy.freesql.datasource.SwitchDataSource)")
      public void dsPointCut(){}
      
      • 通过连接点获取注解
      @Around("dsPointCut()")
      public Object around(ProceedingJoinPoint point) throws Throwable{
      
          MethodSignature signature = (MethodSignature) point.getSignature();
      
          Method method = signature.getMethod();
      
          SwitchDataSource switchDataSource = method.getAnnotation(SwitchDataSource.class);
      
          if (null != switchDataSource) {
      
              DynamicDataSourceContextHolder.setDateSourceType(switchDataSource.value().name());
          }
      
          try {
              return point.proceed();
          }finally {
              //销毁数据源,在执行方法之后
              DynamicDataSourceContextHolder.clearDataSourceType();
          }
      }
      
      • 将ThreadLocal设为当前注解中枚举类的取值
      /*
      * 使用ThreadLocal维护变量,ThreadLocal为每个使用变量的线程提供独立的副本
      * 所以每个线程都可以独立的改变自己的副本,而不会影响其他线程所对应的副本
      * */
      private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
      
      //设置数据源
      public static void setDateSourceType(String dsType){
      
          log.info("切换到{}数据源", dsType);
      
          CONTEXT_HOLDER.set(dsType);
      }
      
      • determineCurrentLookupKey()返回该值
      /*
      * 该方法返回需要使用的DataSource的key值
      * 然后根据这个key从resolveDataSource这个map里取出对应的DataSource
      * 若找不到,则用默认的resolvedDefaultDataSource
      * */
      @Override
      protected Object determineCurrentLookupKey() {
      
          return DynamicDataSourceContextHolder.getDataSourceType();
      }
      
      • Spring以该值为key,切换到对应的数据源
    • 在方法执行后,销毁数据源(切换回默认数据源)


    Druid与Hikari

    • 连接池为Druid时,实现多数据源配置
      注意:注入数据源的过程中,DruidDataSourceBuilder只需要指定在bean上@ConfigurationProperties("spring.datasource.druid.master")即可从配置文件中装配
    • 连接池为Hikari时,实现多数据源配置
      (spring-boot-starter-jdbc默认使用)
      注意:Hikara并不能autoconfigure,显式的开启@ConfigurationProperties支持,需要在启动类上加@EnableConfigurationProperties(DataSourceProperties.class)注解

    测试


    注解

    1. @ConfigurationProperties注解:
      使用@EnableConfigurationProperties开启@ConfigurantionProperties注解的支持。使用该注解的bean可以通过标准方式注册到容器。
      @EnableConfigurationProperties只定义了一个value属性,用于设置一组使用了注解的@ConfigurationProperties的类,可以作为bean定义注册到容器中。
    2. @ConditionOnProperty注解:
      控制某个Configuration是否生效,通过name以及havingValue实现,其中name用来从application.yml中读取某个属性,若值为空,则返回false,若值不为空,则将该值与havingValue指定的值进行比较,如果一样返回true,否则返回false,若返回false,则该configuration不生效,true则生效

    AbstractRoutingDataSource
    Spring提供的动态数据源配置类,充当了DataSource的路由中介,能在运行时,根据某种key值动态切换到真正的DataSource上.

    构造函数

        public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources){
    
                super.setDefaultTargetDataSource(defaultTargetDataSource);
    
                super.setTargetDataSources(targetDataSources);
    
                super.afterPropertiesSet();
            }
    

    targetDataSources目标数据源,存放多数据源

    defaultTargetDataSource默认数据源,初始化、通过key未寻找到数据源、使用切换后数据源方法结束时会使用该数据源

    在DataSourceConfiguration中,调用该构造方法,初始化DynamicDataSource后注入IOC容器

    数据源解析

        @Override
        public void afterPropertiesSet() {
    
            if (this.targetDataSources == null) {
                throw new IllegalArgumentException("Property 'targetDataSources' is required");
            }
    
            this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
    
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = resolveSpecifiedLookupKey(key);
                DataSource dataSource = resolveSpecifiedDataSource(value);
                this.resolvedDataSources.put(lookupKey, dataSource);
            });
    
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }
        }
    

    将构造函数传入数据源解析后分别存为resolvedDataSources和defaultTargetDataSource

    工作机制

    •   @Override
        public Connection getConnection() throws SQLException {
            return determineTargetDataSource().getConnection();
        }
      

      从determineTargetDataSource()中获取连接

    •   protected DataSource determineTargetDataSource() {
            
            Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
      
            Object lookupKey = determineCurrentLookupKey()
            DataSource dataSource = this.resolvedDataSources.get(lookupKey);
      
            if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
                dataSource = this.resolvedDefaultDataSource;
            }
            if (dataSource == null) {
                throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
            }
            return dataSource;
        }
      

      从determineCurrentLookupKey()中获取lookupKey,再去resolvedDataSources中根据lookupKey获取dataSource

      lenientFallback控制在通过lookupKey无法获取到dataSource时,是否使用默认数据源

    •   @Nullable
        protected abstract Object determineCurrentLookupKey();
      

      抽象方法,由实现类返回一个key

    相关文章

      网友评论

        本文标题:动态数据源配置

        本文链接:https://www.haomeiwen.com/subject/jhofpctx.html