网站Logo 花果酿的小花园

动态数据源

huaguoniang
20
2025-11-19

背景:

在我的现在工作的公司中,有时需要从PLM的系统,调用SAP的系统。在一次请求中,想要跨越两个数据源获取数据 执行业务逻辑,就需要使用到动态数据源了。

这次就来借鉴公司的使用,来自己实现一个动态数据源。

实现

1. 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qiang</groupId>
    <artifactId>MyBatis</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
    </dependencies>

</project>

2. application.xml配置文件

server:
  port: 8080
  servlet:
    application-display-name: test

spring:
  datasource:
    dynamic:
      mysql1:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/charge?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
        username: root
        password: 1234

      mysql2:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/javaee?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
        username: root
        password: 1234

mybatis:
  configuration:
    map-underscore-to-camel-case: true 


logging:
  level:
    com.qiang: debug

3. 配置实体类

用于后续通过依赖注入的方式,在各个地方来获取application.xml中的配置

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

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

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

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

}

4. 数据源工具类

public class DataSourceUtil {

    public static final String DB1 = "mysql1";
    public static final String DB2 = "mysql2";

    // 将数据源保存到线程私有空间中,线程之间互不影响
    private final static ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static String getDB() {
        return contextHolder.get();
    }

    public static void setDB(String dbType) {
        contextHolder.set(dbType);
    }

    public static void clearDB() {
        contextHolder.remove();
    }

}

数据源工具中,使用TreadLocal来存储当前线程的数据源 “Key“,好处就是:

  1. 在任何地方,都能够方便的获取和设置

  2. ThreadLocal是每个线程之间独立拥有的空间,只要注意好释放,线程和线程之间就不会互相影响冲突

5. DynamicDataSource类

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceUtil.getDB();
    }
}

先简单看一下AbstractRoutingDataSource 类中的属性成员

  • targetDataSources是用来存放我们系统全部数据源的地方

  • defaultTargetDataSource是给我们的动态数据源指定了一个默认的数据源

AbstractRoutingDataSource 因为实现了DataSource,能够给MyBatis这种ORM框架提供了获取Connection连接的API。

6. DataSourceConfig动态数据源配置

@Configuration
public class DataSouceConfig {

    private final DatabaseConfig1 databaseConfig1;

    private final DatabaseConfig2 databaseConfig2;

    public DataSouceConfig(DatabaseConfig1 databaseConfig1, DatabaseConfig2 databaseConfig2) {
        this.databaseConfig1 = databaseConfig1;
        this.databaseConfig2 = databaseConfig2;
    }

    @Bean("mysql1")
    public DataSource dataSourceMySQL1(){
        return DataSourceBuilder.create()
                .driverClassName(databaseConfig1.getDriverClassName())
                .url(databaseConfig1.getUrl())
                .password(databaseConfig1.getPassword())
                .username(databaseConfig1.getUsername())
                .build();
    }

    @Bean("mysql2")
    public DataSource dataSourceMySQL2(){
        return DataSourceBuilder.create()
                .driverClassName(databaseConfig2.getDriverClassName())
                .url(databaseConfig2.getUrl())
                .password(databaseConfig2.getPassword())
                .username(databaseConfig2.getUsername())
                .build();
    }

    @Bean("dynamic")
    @Primary
    public DataSource dataSourceDynamic(){
        //创建了一个动态数据源对象实例
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //设置默认的数据源
        dynamicDataSource.setDefaultTargetDataSource(dataSourceMySQL1());
        HashMap<Object, Object> map = new HashMap<>();
        map.put("mysql1", dataSourceMySQL1());
        map.put("mysql2", dataSourceMySQL2());
        //将系统的所有数据源作为一个map传递给TargetDataSources
        dynamicDataSource.setTargetDataSources(map);
        return dynamicDataSource;
    }
}

这个Map传到DynamicDataSource 的defaultTargetDataSource之后,会交给解析器解析,解析成resolvedDataSources(也是一个map),区别就是resolvedDataSources是直接能够使用的DataSource对象

如何理解这个能够直接使用的DataSource对象?直接看源码

其实我们在config中配置的方式就已经是能够直接使用的DataSource对象了,但是AbstractRoutingDataSource 还提供了另外一种使用 字符串 配置的方式。

会有专门的解析器JndiDataSourceLookup去解析字符串,然后最后也是DataSource对象

这里不过多解析,因为一般正常情况使用都是直接配置DataSource对象

回过头,Map ---- >defaultTargetDataSource ---- >resolvedDataSources(解析之后) 这个流程走完,数据源的初始化就完成了,系统中已经有了多个数据源了。接下来就是使用了。

7. 使用

@RequiredArgsConstructor
@Service
public class QueryServiceImpl implements QueryService {
    private final FavoriteMapper favoriteMapper;  //表放在数据源1 中  ————MySQL1
    private final ProvinceMapper provinceMapper;  //表放在数据源2 中  ————MySQL2
    @Override
    public String query() {
        try{
            //先获取数据源1  默认的数据源,获取favorite
            DataSourceUtil.setDB(DataSourceUtil.DB1);
            List<Favorite> favoriteList =favoriteMapper.selectAll();

            //获取数据源2   获取province
            DataSourceUtil.setDB(DataSourceUtil.DB2);
            List<Province> provinceList = provinceMapper.selectAll();

            return "success";
        }catch (Exception e){
            e.printStackTrace();
            return "fail";
        }finally {
            DataSourceUtil.clearDB();
        }
    }
}
<select id="selectAll" resultType="com.qiang.entity.Favorite">
        select * from favorite
 </select>
<select id="selectAll" resultType="com.qiang.entity.Province">
        select * from province
</select>

这里写了一个service,然后favoriteMapper和provinceMapper中的SQL语句分别属于不同的数据源。

  1. 中间通过DataSourceUtil.setDB()方法,去设置ThreadLocal中的数据源。

  2. 然后在MyBatis真正执行的时候,会通过Spring IOC容器去找数据源的Bean,也就是我们在Config中配置的动态数据源dynamic中(虽然有多个,但是dynamic中我们指定了@Primary)

  3. 当识别到是继承了AbstractRoutingDataSource 类的数据源,就会去找determineCurrentLookupKey()这个方法来确定用户指定的数据源Key(具体的获取方式由你来指定,我上面就是配合ThreadLocal 直接return DataSourceUtil.getDB();了)。

  4. 比如我们上面指定了DB1——mysql1,就会返回"mysql1"这个key。

  5. 有了这个key,就可以到resolvedDataSources(解析过的map集合)中找到对应的DataSource对象了。

  6. 后续MyBatis就可以正常的getConnection了

2~6步骤,都是Spring和MyBatis帮我们做了,我们只需要使用DataSourceUtil工具类简单的调用set和get就能够执行数据源的切换,是不是很方便~(当然,最后不要忘记ThreadLocal的特性,用完之后释放clear一下)

优化配置类

上面的config1和config2不够灵活,下面使用一种动态的方式,可以将所有的数据源全部注入

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DataSourceProperty {

    private String driverClassName;
    private String url;
    private String username;
    private String password;
}
@Component
@Data
@ConfigurationProperties(prefix = "spring.datasource.dynamic")
public class DatabaseConfig {

    public Map<String,DataSourceProperty> mysql = new HashMap<>();
}

@Configuration
@RequiredArgsConstructor
public class DataSouceConfig {
    private final DatabaseConfig databaseConfig;

    @Bean("dataSourceMap")
    public Map<String,DataSource> dataSourceMap(){
        HashMap<String, DataSource> map = new HashMap<>();
        for (Map.Entry<String, DataSourceProperty> entry : databaseConfig.getMysql().entrySet()) {
            DataSource dataSource = buildDataSource(entry.getValue());
            map.put(entry.getKey(),dataSource);
        }
        return map;
    }

    @Bean("dynamic")
    public DataSource dataSourceDynamic(Map<String,DataSource>  map){
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(map.get("mysql1"));
        dynamicDataSource.setTargetDataSources(new HashMap<>(map));
        return dynamicDataSource;
    }

    private DataSource buildDataSource(DataSourceProperty property) {
        return DataSourceBuilder.create()
                .driverClassName(property.getDriverClassName())
                .url(property.getUrl())
                .password(property.getPassword())
                .username(property.getUsername())
                .build();
    }
}

动态数据源事务

加入说我们想要在动态数据源的场景中,开始事务,如果直接像上面这样加,就会出错。

提示第二个数据源中(mysql2)的表不存在,为啥呢?

其实是因为在方法上加上了@Transactional,会在进入方法之前,将与数据库的连接 绑定到 当前线程了。这个连接,自然就是 动态数据源中的 默认数据源。

后续再怎么DataSourceUtil.set()切换数据源,都是没有用的。所以第二个表就报错了,因为使用连接是默认的数据源(第一个数据源mysql1)

那要怎么办呢?只能每个数据源单独开启事务。将操作拆分。

@Test
    public void test3(){
        DataSourceUtil.setDB(DataSourceUtil.DB1);
        queryService.insertMySQL1();
        DataSourceUtil.setDB(DataSourceUtil.DB2);
        queryService.insertMySQL2();
    }
@Transactional
    public void insertMySQL1(){
        favoriteMapper.insert(new Favorite(999, "哇哈哈mysql1"));
    }


    @Transactional
    public void insertMySQL2(){
        provinceMapper.insert(new Province(null,"999","感冒灵mysql2"));
        int a = 1/0;  // 模拟Exception
    }

在写入两个数据库之后,在第二个事务那里抛出了Exception。

按照我们想要的,两个步骤,两个事务,第一个事务没有出错,会提交,第二个事务出错了,会回滚。

验证成功。

总结

到最后,总结一下:

  1. 我们通过自己手动配置了动态数据源,也简单实验了动态数据源的切换

  2. 优化了配置类的写法,不需要每次添加数据源,都要增加一个Bean,简化了代码

  3. 实验了一下动态数据源场景下的事务

动物装饰