Spring Boot 中的多数据源配置方案

多数据源可以理解为多数据库,甚至可以是多个不同类型的数据库,比如一个是MySql,一个是Oracle。随着项目的扩大,有时需要数据库的拆分或者引入另一个数据库,这时就需要配置多个数据源。

SpringBoot中使用多数据源还是比较简单的,为了演示方便,我们在MySql中创建两个数据库:ds1、ds2,并在ds1数据库中创建student表,在ds2数据库中创建teacher表。数据库脚本如下:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------

-- Table structure for student

-- ----------------------------

DROP TABLE IF EXISTS `student`;
CREATE TABLE `student`  (
  `id` varchar(16) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `class` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of student

-- ----------------------------

INSERT INTO `student` VALUES ('123456', 'zhangsan', '北京');
INSERT INTO `student` VALUES ('123457', 'lisi', '上海');

SET FOREIGN_KEY_CHECKS = 1;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------

-- Table structure for teacher

-- ----------------------------

DROP TABLE IF EXISTS `teacher`;
CREATE TABLE `teacher`  (
  `id` varchar(16) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `class` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of teacher

-- ----------------------------

INSERT INTO `teacher` VALUES ('0000001', 'wangwu', '上海');

SET FOREIGN_KEY_CHECKS = 1;

基于MyBatis的多数据源实现

首先创建一个MyBatis项目,项目结构如下:


image.png

这里有一点需要注意, StudentMapper 接口和 TeacherMapper 接口是分开的,它们位于不同子目录下,这个后面会提到。

数据库连接配置

既然是多数据源,数据库连接的信息就有可能存在不同,所以需要在配置文件中配置各个数据源的连接信息(这里使用了druid数据库连接池)。

spring: 
  datasource:
    ds1: #数据源1,默认数据源
      url: jdbc:mysql://localhost:3306/ds1?serverTimezone=GMT&useSSL=false&useUnicode=true&characterEncoding=utf8
      username: root
      password: root
      typ: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      filters: stat
      maxActive: 2
      initialSize: 1
      maxWait: 60000
      minIdle: 1
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxOpenPreparedStatements: 20
      
    ds2: #数据源2
      url: jdbc:mysql://localhost:3306/ds2?serverTimezone=GMT&useSSL=false&useUnicode=true&characterEncoding=utf8
      username: root
      password: root
      typ: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      filters: stat
      maxActive: 2
      initialSize: 1
      maxWait: 60000
      minIdle: 1
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxOpenPreparedStatements: 20

注意不同的数据源要用不同的属性名区分。

重写SpringBoot的数据源配置

1、数据源1的配置

@Configuration
@MapperScan(basePackages = {"com.chou.easyspringboot.multipledatasource.mapper.ds1"}, sqlSessionFactoryRef = "sqlSessionFactory1")
public class Datasource1Configuration {
    @Value("${mybatis.mapper-locations}")
    private String mapperLocation;
    @Value("${spring.datasource.ds1.url}")
    private String jdbcUrl;
    @Value("${spring.datasource.ds1.driver-class-name}")
    private String driverClassName;
    @Value("${spring.datasource.ds1.username}")
    private String username;
    @Value("${spring.datasource.ds1.password}")
    private String password;
    @Value("${spring.datasource.ds1.initialSize}")
    private int initialSize;
    @Value("${spring.datasource.ds1.minIdle}")
    private int minIdle;
    @Value("${spring.datasource.ds1.maxActive}")
    private int maxActive;

    @Bean(name = "dataSource1")
    @Primary
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(jdbcUrl);
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setInitialSize(initialSize);
        dataSource.setMinIdle(minIdle);
        dataSource.setMaxActive(maxActive);

        return dataSource;
    }

    @Bean("sqlSessionFactory1")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource1") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources(mapperLocation));

        return sqlSessionFactoryBean.getObject();
    }

    @Bean("sqlSessionTemplate1")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory1") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean("transactionManager1")
    public DataSourceTransactionManager transactionManager(@Qualifier("dataSource1")DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

2、数据源2的配置

@Configuration
@MapperScan(basePackages = {"com.chou.easyspringboot.multipledatasource.mapper.ds2"}, sqlSessionFactoryRef = "sqlSessionFactory2")
public class Datasource2Configuration {
    @Value("${mybatis.mapper-locations}")
    private String mapperLocation;
    @Value("${spring.datasource.ds2.url}")
    private String jdbcUrl;
    @Value("${spring.datasource.ds2.driver-class-name}")
    private String driverClassName;
    @Value("${spring.datasource.ds2.username}")
    private String username;
    @Value("${spring.datasource.ds2.password}")
    private String password;
    @Value("${spring.datasource.ds2.initialSize}")
    private int initialSize;
    @Value("${spring.datasource.ds2.minIdle}")
    private int minIdle;
    @Value("${spring.datasource.ds2.maxActive}")
    private int maxActive;

    @Bean(name = "dataSource2")
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(jdbcUrl);
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setInitialSize(initialSize);
        dataSource.setMinIdle(minIdle);
        dataSource.setMaxActive(maxActive);

        return dataSource;
    }

    @Bean("sqlSessionFactory2")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource2") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources(mapperLocation));

        return sqlSessionFactoryBean.getObject();
    }

    @Bean("sqlSessionTemplate2")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory2") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean("transactionManager2")
    public DataSourceTransactionManager transactionManager(@Qualifier("dataSource2") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

这里和单数据源不同的地方在于对 dataSourcesqlSessionFactorysqlSessionTemplatetransactionManager 都进行了单独的配置。另外,数据源1和数据源2主要存在两点不同:

  1. @MapperScan 中的包扫描路径不一样,数据源1只扫描 com.chou.easyspringboot.multipledatasource.mapper.ds1 路径下的 Mapper ,数据源2负责 com.chou.easyspringboot.multipledatasource.mapper.ds2下Mapper ,所以在前面创建的时候我们要把 StudentMapperTeacherMapper 分开。因为在这里已经配置了 @MapperScan ,所以在启动类中必须不能在存在 @MapperScan 注解

  2. 数据源1中多一个 @Primary 注解,这是告诉Spring我们使用的默认数据源,也是多数据源项目中必不可少的。

测试

编写相应的Controller和Service层代码,查询所有的Student和Teacher信息,并使用postman模拟发送请求,会有如下的运行结果:

  • 查询所有的Student


    image.png
  • 查询所有Teacher


    image.png

我们连续发送两个不同的请求,都得出了想要的结果,说明MyBatis自动帮我们切换到了对应的数据源上。

基于自定义注解实现多数据源

上面我们提高到数据源自动切换主要依靠MyBatis,如果项目中没有使用MyBatis该如何做呢?

多数据源自动切换原理

这里介绍一种基于自定义注解的方法实现多数据源的动态切换。SpringBoot中有一个 AbstractRoutingDataSource 抽象类,我们可以实现其抽象方法 determineCurrentLookupKey() 去指定数据源。并通过AOP编写自定义注解处理类,在sql语句执行前,切换到自定义注解中设置的数据源以实现数据源的自动切换。

数据库连接配置

同上配置两个数据库连接信息。

创建数据源存放类

DataSource 是和线程绑在一起的,因此,我们需要一个线程安全的类来存放 DataSource ,在determineCurrentLookupKey() 中通过该类获取数据源。

AbstractRoutingDataSource 类中, DataSource 以键值对的形式保存,可以使用 ThreadLocal 来保存key,从而实现多数据源的自动切换。

public class DataSourceContextHolder {
    private static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);

    // 使用ThreadLocal线程安全的使用变量副本
    private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal();

    /**
     * 设置数据源
     * */
    public static void setDataSource(String dataSource) {
        logger.info("切换到数据源:{}", dataSource);
        CONTEXT_HOLDER.set(dataSource);
    }

    /**
     * 获取数据源
     * */
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源
     * */
    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

数据源持有类定义了三个方法,分别用于数据源的设置、获取和清除。

创建数据源枚举类

public enum DataSourceEnum {
    PRIMARY, //默认数据源
    DATASOURCE1
}

实现 determineCurrentLookupKey 方法指定数据源

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

配置数据源

@Configuration
public class DynamicDataSourceConfiguration {
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.ds1")
    public DataSource primaryDataSource(){
        return new DruidDataSource();
    }

    @Bean(name = "dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.ds2")
    public DataSource dataSource1(){
        return new DruidDataSource();
    }

    @Bean("dynamicDataSource")
    @Primary
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //配置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(primaryDataSource());

        //配置多数据源
        HashMap dataSourceMap = new HashMap();
        dataSourceMap.put(DataSourceEnum.PRIMARY.name(),primaryDataSource());
        dataSourceMap.put(DataSourceEnum.DATASOURCE1.name(),dataSource1());
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        return dynamicDataSource;

    }
}

自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    DataSourceEnum value() default DataSourceEnum.PRIMARY;
}

自定义注解指定作用于方法上并在运行期生效(可以在网上查下如何自定义注解,这里不在讲述)。

AOP拦截

通过AOP在执行sql语句前拦截,并切换到自定义注解指定的数据源上。有一点需要注意,自定义数据源注解与 @Transaction 注解同一个方法时会先执行 @Transaction ,即获取数据源在切换数据源之前,所以会导致自定义注解失效,因此需要使用 @Order (@Order的value越小,就越先执行),保证该AOP在 @Transactional 之前执行。

@Aspect
@Component
@Order(-1)
public class DataSourceAspect {
    @Pointcut("@annotation(com.chou.easyspringboot.multipledatasource.annotation.DataSource)")
    public void dataSourcePointCut() {

    }

    @Around("dataSourcePointCut()")
    public Object dataSourceArround(ProceedingJoinPoint proceed) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) proceed.getSignature();
        Method method = methodSignature.getMethod();
        DataSource dataSource = method.getAnnotation(DataSource.class);
        if(dataSource != null) {
            DataSourceContextHolder.setDataSource(dataSource.value().name());
        }

        try {
            return proceed.proceed();
        } finally {
            // 方法执行后销毁数据源
            DataSourceContextHolder.clearDataSource();
        }
    }
}

创建启动类,编写Controller、Service层代码

需要在启动类的 @SpringBootApplication 注解中移除DataSource自动配置类,否则会默认自动配置,而不会使用我们自定义的DataSource,并且启动会有循环依赖的错误。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class EasyspringbootMultipledatasourceApplication {

   public static void main(String[] args) {
      SpringApplication.run(EasyspringbootMultipledatasourceApplication.class, args);
   }

}

测试

  • 查询所有Student


    image.png
  • 查询所有Teacher


    image.png

我们得到了正确的结果,数据源自动切换了。

项目完整代码: https://github.com/Mark-Chou20/easy-springboot

你可能感兴趣的:(Spring Boot 中的多数据源配置方案)