在 SpringBoot 项目中多数据源切换

使用dynamic-datasource-spring-boot-starter库

添加依赖

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>3.4.1</version>
</dependency>

添加配置

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:mysql://ip:port/testdb?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: xxx
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave:
          url: jdbc:mysql:/ip:port/testdb2?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: xxx
          driver-class-name: com.mysql.cj.jdbc.Driver

代码使用

@Service
@DS("master")
public class UserService {

  @Autowired
  private UserRepository userRepo;

  public void queryAndUpdate(List<Integer> deptIds) {
    List<User> userList = userRepo.findAll();
    System.out.println(userList.size());
  }
}

通过@DS注解来切换数据源,使用还是很简单的。

原理分析

  1. 自动配置类 DynamicDataSourceAutoConfiguration 会配置 DynamicDataSourceAnnotationInterceptor 拦截器及 DynamicRoutingDataSource 动态数据源类。
  2. DynamicDataSourceAnnotationInterceptor 拦截@DS注解,获取注解配置的数据源名称,如 master,存储到 DynamicDataSourceContextHolder 中,内部是一个 ThreadLocal。
  3. DynamicRoutingDataSource 在对象初始化时(afterPropertiesSet方法)会根据 yml 配置创建多个数据源 dataSourceMap,包含 master 和 slave,在 getConnection() 时,从 DynamicDataSourceContextHolder 中取出配置的数据源名称,根据名称从 dataSourceMap 中获取到真正的 DataSource。

自己实现一个简单的动态切换库

添加配置

和上面配置保持一致

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:mysql://ip:port/testdb?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: xxx
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave:
          url: jdbc:mysql:/ip:port/testdb2?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: xxx
          driver-class-name: com.mysql.cj.jdbc.Driver

定义实体类

用来接收 yml 配置

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master")
public class MasterDataSourceProperty {

    private String url;
    private String username;
    private String password;
    private String driverClassName;

}
@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.slave")
public class SlaveDataSourceProperty {

    private String url;
    private String username;
    private String password;
    private String driverClassName;

}

定义ContextHolder

public class DBContextHolder {

    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 绑定当前线程数据源路由的key
     * 使用完成后必须调用removeRouteKey()方法删除
     * @param dataSource
     */
    public static void setDataSource(String dataSource) {
        CONTEXT_HOLDER.set(dataSource);
    }

    /**
     * 获取当前线程的数据源路由的key
     * @return
     */
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 删除与当前线程绑定的数据源路由的key
     */
    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

存储拦截器获取到的数据源名称,供后续使用

定义数据源配置

@Configuration
@Slf4j
public class DataSourceConfig {

    @Value("${spring.datasource.dynamic.primary}")
    private String defaultTargetDataSource;

    /**
     * 主数据源
     *
     * @return
     */
    @Bean("masterDataSource")
    public DataSource masterDataSource(MasterDataSourceProperty dataSourceProperty) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dataSourceProperty.getUrl());
        dataSource.setUsername(dataSourceProperty.getUsername());
        dataSource.setPassword(dataSourceProperty.getPassword());
        dataSource.setDriverClassName(dataSourceProperty.getDriverClassName());
        return dataSource;
    }

    /**
     * 从数据源
     *
     * @return
     */
    @Bean("slaveDataSource")
    public DataSource slaveDataSource(SlaveDataSourceProperty dataSourceProperty) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dataSourceProperty.getUrl());
        dataSource.setUsername(dataSourceProperty.getUsername());
        dataSource.setPassword(dataSourceProperty.getPassword());
        dataSource.setDriverClassName(dataSourceProperty.getDriverClassName());
        return dataSource;
    }

    @Primary
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource);
        targetDataSources.put("slave", slaveDataSource);
        return new DynamicDataSource(defaultTargetDataSource, targetDataSources);
    }

    public static class DynamicDataSource extends AbstractRoutingDataSource {

        /**
         * 实现数据源切换要扩展的方法,
         * 通过返回值获取指定的数据源
         *
         * @return
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return DBContextHolder.getDataSource();
        }

        public DynamicDataSource(String defaultTargetDataSource, Map<Object, Object> targetDataSources) {
            //设置默认数据源
            super.setDefaultTargetDataSource(targetDataSources.get(defaultTargetDataSource));
            //设置数据源列表
            super.setTargetDataSources(targetDataSources);
        }

    }
}

此配置类在低版本的 SpringBoot 项目中会报错

 * The dependencies of some of the beans in the application context form a cycle:
 *
 *    masterUserService
 *       ↓
 *    userMapper defined in file [C:\D-myfiles\java\code_resp\gitee\spring_ds\target\classes\com\imooc\ds\service\UserMapper.class]
 *       ↓
 *    sqlSessionFactory defined in class path resource [com/baomidou/mybatisplus/autoconfigure/MybatisPlusAutoConfiguration.class]
 * ┌─────┐
 * |  dynamicDataSource defined in class path resource [com/imooc/ds/custom/DataSourceConfig.class]
 * ↑     ↓
 * |  masterDataSource defined in class path resource [com/imooc/ds/custom/DataSourceConfig.class]
 * ↑     ↓
 * |  org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker
 * └─────┘

具体原因为:

  1. DynamicDataSource 在创建 Bean 时依赖 masterDataSource
  2. 创建 masterDataSource 的 Bean 后,会触发 DataSourceInitializerPostProcessor 处理器的 postProcessAfterInitialization()方法,此方法会创建 DataSourceInitializerInvoker 的 Bean 实例
  3. DataSourceInitializerInvoker 又会依赖 DataSource的 Bean 实例,这里就会依赖 DynamicDataSource(因为我们设置了@Primary注解),到这里就形成了循环依赖。

Spring 框架不支持构造器注入方式的循环依赖,但支持属性注入的循环依赖,具体原理参考 Spring源码分析之循环引用 ,所以我们的解决方案就是将构造器注入改为属性注入,具体代码见下文。

定义数据源配置(解决循环依赖)

@Configuration
@Slf4j
public class DataSourceConfig2 {

    @Autowired
    private MasterDataSourceProperty masterDataSourceProperty;
    @Autowired
    private SlaveDataSourceProperty slaveDataSourceProperty;

    /**
     * 主数据源
     *
     * @return
     */
    @Bean("masterDataSource")
    public DataSource masterDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(masterDataSourceProperty.getUrl());
        dataSource.setUsername(masterDataSourceProperty.getUsername());
        dataSource.setPassword(masterDataSourceProperty.getPassword());
        dataSource.setDriverClassName(masterDataSourceProperty.getDriverClassName());
        return dataSource;
    }

    /**
     * 从数据源
     *
     * @return
     */
    @Bean("slaveDataSource")
    public DataSource slaveDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(slaveDataSourceProperty.getUrl());
        dataSource.setUsername(slaveDataSourceProperty.getUsername());
        dataSource.setPassword(slaveDataSourceProperty.getPassword());
        dataSource.setDriverClassName(slaveDataSourceProperty.getDriverClassName());
        return dataSource;
    }

    @Primary
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource2 dataSource() {
        return new DynamicDataSource2();
    }

    /**
     * 正确的配置,在DataSourceInitializerPostProcessor处理之后再初始化数据
     */
    public static class DynamicDataSource2 extends AbstractRoutingDataSource {

        @Autowired
        @Qualifier("masterDataSource")
        private DataSource masterDataSource;
        @Autowired
        @Qualifier("slaveDataSource")
        private DataSource slaveDataSource;
        @Value("${spring.datasource.dynamic.primary}")
        private String defaultTargetDataSource;

        /**
         * 实现数据源切换要扩展的方法,
         * 通过返回值获取指定的数据源
         *
         * @return
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return DBContextHolder.getDataSource();
        }

        public DynamicDataSource2() {
        }

        @Override
        public void afterPropertiesSet() {
            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put("master", masterDataSource);
            targetDataSources.put("slave", slaveDataSource);
            //设置默认数据源
            super.setDefaultTargetDataSource(targetDataSources.get(defaultTargetDataSource));
            //设置数据源列表
            super.setTargetDataSources(targetDataSources);
            super.afterPropertiesSet();
        }
    }
}

通过@Autowired注解在 Bean初始化时注入,而不是在 Bean 实例化(创建对象)时就注入,就可以解决循环依赖的问题了。或者下面这种方式也可以,还更好理解一些。

@Configuration
@Slf4j
public class DataSourceConfig3 {

    @Primary
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource3 dataSource() {
        return new DynamicDataSource3();
    }

    /**
     * 正确的配置,在DataSourceInitializerPostProcessor处理之后再初始化数据
     */
    public static class DynamicDataSource3 extends AbstractRoutingDataSource {

        @Value("${spring.datasource.dynamic.primary}")
        private String defaultTargetDataSource;

        @Autowired
        private MasterDataSourceProperty masterDataSourceProperty;
        @Autowired
        private SlaveDataSourceProperty slaveDataSourceProperty;

        /**
         * 实现数据源切换要扩展的方法,
         * 通过返回值获取指定的数据源
         *
         * @return
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return DBContextHolder.getDataSource();
        }

        public DynamicDataSource3() {
        }

        @Override
        public void afterPropertiesSet() {
            Map<Object, Object> targetDataSources = createTargetDataSources();
            //设置默认数据源
            super.setDefaultTargetDataSource(targetDataSources.get(defaultTargetDataSource));
            //设置数据源列表
            super.setTargetDataSources(targetDataSources);
            super.afterPropertiesSet();
        }

        private Map<Object, Object> createTargetDataSources() {
            HikariDataSource masterDataSource = new HikariDataSource();
            masterDataSource.setJdbcUrl(masterDataSourceProperty.getUrl());
            masterDataSource.setUsername(masterDataSourceProperty.getUsername());
            masterDataSource.setPassword(masterDataSourceProperty.getPassword());
            masterDataSource.setDriverClassName(masterDataSourceProperty.getDriverClassName());

            HikariDataSource slaveDataSource = new HikariDataSource();
            slaveDataSource.setJdbcUrl(slaveDataSourceProperty.getUrl());
            slaveDataSource.setUsername(slaveDataSourceProperty.getUsername());
            slaveDataSource.setPassword(slaveDataSourceProperty.getPassword());
            slaveDataSource.setDriverClassName(slaveDataSourceProperty.getDriverClassName());

            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put("master", masterDataSource);
            targetDataSources.put("slave", slaveDataSource);
            return targetDataSources;
        }
    }
}

dynamic-datasource-spring-boot-starter 库中就是这种方式。

定义拦截器及数据源切换的注解

@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
    String value();
}
@Slf4j
@Aspect
@Component
public class DataSourceAspect {

    /**
     * 在类上和方法上都会拦截到
     */
    @Pointcut(value = "@annotation(com.imooc.ds.custom.TargetDataSource) " +
            "|| @within(com.imooc.ds.custom.TargetDataSource)")
    public void dataSourcePointCut() {
    }

    @Before("dataSourcePointCut()")
    public void before(JoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        String tragetDataSource = getTragetDataSource(method);
        DBContextHolder.setDataSource(tragetDataSource);
        log.info("当前线程名称:{},当前数据源:{}", Thread.currentThread().getName(), tragetDataSource);
    }

    private String getTragetDataSource(Method method) {
        TargetDataSource annotation = method.getAnnotation(TargetDataSource.class);
        if (Objects.nonNull(annotation)) {
            return annotation.value();
        }
        Class<?> declaringClass = method.getDeclaringClass();
        annotation = declaringClass.getAnnotation(TargetDataSource.class);
        if (Objects.nonNull(annotation)) {
            return annotation.value();
        }
        return null;
    }

    @After("dataSourcePointCut()")
    public void doAfterReturning() {
        DBContextHolder.clearDataSource();
    }
}

拦截类或者方法上的@TargetDataSource 注解,方法优先级更高。

参考

dynamic-datasource-gitee仓库

posted @ 2024-02-08 20:12  strongmore  阅读(429)  评论(0编辑  收藏  举报