191202-Spirng动态多数据源.md 10 KB

项目220510-dynamicDataSource仓库地址

原理

java提供了一个接口 java.sql.DataSource 用来获取数据库连接 getConnnection(),动态多数据源就是在该接口基础上实现的。

Spring中 AbstractRoutingDataSource 实现了该 DataSource 接口

@Override
public Connection getConnection() throws SQLException {
    return determineTargetDataSource().getConnection();
}
...	
/**
 * 根据determineCurrentLookupKey()决定使用哪个数据源,如果为空,则使用默认数据源
 */
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;
}

/**
 * 抽象方法,返回数据源map的key,用来决定具体使用哪个数据源
 */
@Nullable
protected abstract Object determineCurrentLookupKey();

抽象方法 determineCurrentLookupKey() 是暴露给开发者的,我们可以通过实现该方法在不同数据源之间切换。

SpringBoot实践

1. 配置多数据源

在 application.yml 如下配置

spring:
  datasource:
    # 数据源类型
    type: com.alibaba.druid.pool.DruidDataSource
    # 默认数据源
    default-datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/db0?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&allowMultiQueries=true&serverTimezone=GMT%2B8
      username: root
      password: 123456

    # 多数据源
    target-datasources:
      datasource1:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&allowMultiQueries=true&serverTimezone=GMT%2B8
        username: root
        password: 123456

      datasource2:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&allowMultiQueries=true&serverTimezone=GMT%2B8
        username: root
        password: 123456

    # druid 默认配置
    druid:
      # 初始连接数
      initial-size: 10
      # 最大连接池数量
      max-active: 100
      # 最小连接池数量
      min-idle: 10
      # 配置获取连接等待超时的时间
      max-wait: 60000
      # 打开PSCache,并且指定每个连接上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      stat-view-servlet:
        enabled: true
        url-pattern: /monitor/druid/*
      filter:
        stat:
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: false
        wall:
          config:
            multi-statement-allow: true

# MyBatis
mybatis:
  # 搜索指定包别名
  typeAliasesPackage: com.guide
  # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml

此处配置的名称(如 defaultDataSource、targetDataSources)的命名并无特殊要求,只要和下面第3步的 DataSourceConfig 中对应起来就可以

使用 Druid 数据源的话,要在 pom.xml 中引入依赖

<!--阿里数据库连接池 -->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid-spring-boot-starter</artifactId>
	<version>1.1.10</version>
</dependency>

2. 实现数据源

DynamicDataSource 动态数据源,在多个数据源之间切换

public class DynamicDataSource extends AbstractRoutingDataSource {

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

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}

DataSourceContextHolder 数据源上下文,使用线程变量来存储代表当前使用的数据源的key值(每个key值都对应一个数据源,用以区分多数据源)

public class DataSourceContextHolder {

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

    public static void setDataSourceType(String dsType) {
        CONTEXT_HOLDER.set(dsType);
    }

    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    public static void removeDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

DataSourceType 数据源对应的key(其实单纯的用字符串来表示数据源,替换枚举类DataSourceType也是可以的,但是写代码时要注意字符串统一)

public enum  DataSourceType {
    /** 默认数据源key */
    DEFAULT_DATASOURCE,

    /** 数据源1key*/
    DATASOURCE1,

    /** 数据源2key*/
    DATASOURCE2;
}

3. 将数据源添加到 Spring 容器中

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.default-datasource")
    public DataSource defaultDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.target-datasources.datasource1")
    public DataSource dataSource1() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.target-datasources.datasource2")
    public DataSource dataSource2() {
        return DruidDataSourceBuilder.create().build();
    }

	/**
	 * Spring 容器里实际上有4个数据源bean
	 * 分别是 defaultDataSource、dataSource1、dataSource2、dynamicDataSource
	 * 自动注入dataSource时,spring就会疑惑到底注入哪一个bean
	 * 在dynamicDataSource上加注解@Primary就是为了解决这个问题,加了这个注解就表示优先注入这个bean
	 */ 
    @Bean
    @Primary
    public DataSource dynamicDataSource(DataSource defaultDataSource, DataSource dataSource1, DataSource dataSource2) {
        // 注意:该方法的参数名称要和前面前面三个datasource对象在Spring容器中的bean名称一样
        // 或者使用 @Qualifier 指定具体的bean
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.DEFAULT_DATASOURCE.name(), defaultDataSource);
        targetDataSources.put(DataSourceType.DATASOURCE1.name(), dataSource1);
        targetDataSources.put(DataSourceType.DATASOURCE2.name(), dataSource2);
        return new DynamicDataSource(defaultDataSource, targetDataSources);
    }
}

测试

测试代码

为了方便,省略了 Service 层

@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private TestMapper testMapper;

    @GetMapping
    public List<Map<String, Object>> test(String dataSourceIndex) {
        // 根据参数值的不同,切换数据源
        if ("1".equals(dataSourceIndex)) {
            DataSourceContextHolder.setDataSourceType(DataSourceType.DATASOURCE1.name());
        } else if ("2".equals(dataSourceIndex)) {
            DataSourceContextHolder.setDataSourceType(DataSourceType.DATASOURCE2.name());
        }
        List<Map<String, Object>> mapList = testMapper.selectList();
        // 清除线程内部变量数据源key
        DataSourceContextHolder.removeDataSourceType();
        return mapList;
    }
}

TestMapper

@Repository
public interface TestMapper {
    /**
     * 查询列表
     * @return
     */
    List<Map<String, Object>> selectList();
}

TestMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.guide.datasource.mapper.TestMapper">
	<select id="selectList" resultType="java.util.Map">
		SELECT * FROM test
	</select>
</mapper>

启动类

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})  
@MapperScan("com.guide.*.mapper")  
public class DatasourceApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(DatasourceApplication.class, args);  
    }  
  
}

测试数据

别忘了要准备数据哦! 下面SQL语句,创建3个数据库,然后在3个数据库中都创建一张test表,并各自插入不同的数据。

-- 创建数据库
create database db0 character set utf8 collate utf8_general_ci;
create database db1 character set utf8 collate utf8_general_ci;
create database db2 character set utf8 collate utf8_general_ci;

-- 在数据库db1下执行以下SQL
use db0;
create table test(
	id int(11) primary key auto_increment,
	name varchar(20)
) ;
insert into test(name) values('张三');


-- 在数据库db1下执行以下SQL
use db1;
create table test(
	id int(11) primary key auto_increment,
	name varchar(20)
) ;
insert into test(name) values('李四');

-- 在数据库db2下执行以下SQL
use db2;
create table test(
	id int(11) primary key auto_increment,
	name varchar(20)
) ;
insert into test(name) values('王五');

OK,一切准备就绪,启动应用吧!!!

测试结果

访问 http://localhost:8080/test ,此时使用的是默认数据源 defaultDataSource,页面显示:

[{"name":"张三","id":1}]

访问 http://localhost:8080/test?dataSourceIndex=1 ,使用数据源 dataSource1,页面显示:

[{"name":"李四","id":1}]

访问 http://localhost:8080/test?dataSourceIndex=2 ,使用数据源 dataSource2,页面显示:

[{"name":"王五","id":1}]