背景:
在我的现在工作的公司中,有时需要从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“,好处就是:
在任何地方,都能够方便的获取和设置
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语句分别属于不同的数据源。
中间通过DataSourceUtil.setDB()方法,去设置ThreadLocal中的数据源。
然后在MyBatis真正执行的时候,会通过Spring IOC容器去找数据源的Bean,也就是我们在Config中配置的动态数据源dynamic中(虽然有多个,但是dynamic中我们指定了@Primary)
当识别到是继承了AbstractRoutingDataSource 类的数据源,就会去找determineCurrentLookupKey()这个方法来确定用户指定的数据源Key(具体的获取方式由你来指定,我上面就是配合ThreadLocal 直接return DataSourceUtil.getDB();了)。
比如我们上面指定了DB1——mysql1,就会返回"mysql1"这个key。
有了这个key,就可以到resolvedDataSources(解析过的map集合)中找到对应的DataSource对象了。
后续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。
按照我们想要的,两个步骤,两个事务,第一个事务没有出错,会提交,第二个事务出错了,会回滚。


验证成功。
总结
到最后,总结一下:
我们通过自己手动配置了动态数据源,也简单实验了动态数据源的切换
优化了配置类的写法,不需要每次添加数据源,都要增加一个Bean,简化了代码
实验了一下动态数据源场景下的事务