SpringBoot实现多数据源,动态切换数据源

需求

单体系统和SaaS系统之间的数据传输,单体系统只有一个数据库链接,SaaS系统拥有N个数据库链接,并且有单独的租户系统来维护所有租户的数据库链接。

理论

SpringBoot内置了一个AbstractRoutingDataSource抽象类,将所有数据源装入map,然后可以根据不同的key返回不同的数据源。当SpringBoot开始执行连接数据库之前会执行determineCurrentLookupKey()方法,这个方法返回的数据将作为key去map中查找相应的数据源。

关于AbstractRoutingDataSource抽象类的简单说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
// 目标数据源map集合,存储将要切换的多数据源bean信息
@Nullable
private Map<Object, Object> targetDataSources;
// 未指定数据源时的默认数据源对象
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
// 数据源查找接口,通过该接口的getDataSource(String dataSourceName)获取数据源信息
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
// 解析targetDataSources之后的DataSource的map集合
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;

// 将targetDataSources的内容转化一下放到resolvedDataSources中,将defaultTargetDataSource转为DataSource赋值给resolvedDefaultDataSource
public void afterPropertiesSet() {
// 如果目标数据源为空,会抛出异常,在系统配置时应至少传入一个数据源
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
// 初始化resolvedDataSources的大小
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
// 遍历目标数据源信息map集合,对其中的key,value进行解析
this.targetDataSources.forEach((key, value) -> {
// resolveSpecifiedLookupKey方法没有做任何处理,只是将key继续返回
Object lookupKey = this.resolveSpecifiedLookupKey(key);
// 将目标数据源map集合中的value值(德鲁伊数据源信息)转为DataSource类型
DataSource dataSource = this.resolveSpecifiedDataSource(value);
// 将解析之后的key,value放入resolvedDataSources集合中
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
// 将默认目标数据源信息解析并赋值给resolvedDefaultDataSource
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}

}
}

protected Object resolveSpecifiedLookupKey(Object lookupKey) {
return lookupKey;
}

protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
if (dataSource instanceof DataSource) {
return (DataSource)dataSource;
} else if (dataSource instanceof String) {
return this.dataSourceLookup.getDataSource((String)dataSource);
} else {
throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
}
}

// 因为AbstractRoutingDataSource继承AbstractDataSource,而AbstractDataSource实现了DataSource接口,所有存在获取数据源连接的方法
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}

public Connection getConnection(String username, String password) throws SQLException {
return this.determineTargetDataSource().getConnection(username, password);
}

protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 调用实现类中重写的determineCurrentLookupKey方法拿到当前线程要使用的数据源的名称
Object lookupKey = this.determineCurrentLookupKey();
// 去解析之后的数据源信息集合中查询该数据源是否存在,如果没有拿到则使用默认数据源resolvedDefaultDataSource
DataSource 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 + "]");
} else {
return dataSource;
}
}

@Nullable
protected abstract Object determineCurrentLookupKey();
}

实践

配置文件

1
2
3
4
5
6
7
8
9
spring:
datasource:
druid:
type: com.alibaba.druid.pool.DruidDataSource
master:
url: jdbc:mysql://xxxx:3306/apis?useSSL=false&useUnicode=true&autoReconnect=true&failOverReadOnly=false
username: root
password: xxxxxx
driver-class-name: com.mysql.jdbc.Driver

包含的类

  • DynamicDataSource # 重写动态数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 动态数据源
* @author HiF
* @date 2023/10/24 21:52
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DynamicDataSource extends AbstractRoutingDataSource {
//备份所有数据源信息,
private Map<Object, Object> defineTargetDataSources;

/**
* 决定当前线程使用哪个数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDynamicDataSourceKey();
}

}
  • DynamicDataSourceHolder # 数据源切换处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 数据源切换处理
* @author HiF
* @date 2023/10/24 20:46
*/
@Slf4j
public class DynamicDataSourceHolder {

/**
* 保存动态数据源名称
*/
private static final ThreadLocal<String> DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();

/**
* 设置/切换数据源,决定当前线程使用哪个数据源
*/
public static void setDynamicDataSourceKey(String key){
log.info("数据源切换为:{}",key);
DYNAMIC_DATASOURCE_KEY.set(key);
}

/**
* 获取动态数据源名称,默认使用mater数据源
*/
public static String getDynamicDataSourceKey(){
String key = DYNAMIC_DATASOURCE_KEY.get();
return key == null ? DataSourceConstant.MASTER : key;
}

/**
* 移除当前数据源
*/
public static void removeDynamicDataSourceKey(){
log.info("移除数据源:{}",DYNAMIC_DATASOURCE_KEY.get());
DYNAMIC_DATASOURCE_KEY.remove();
}
}
  • DruidConfig # 数据源配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 数据源配置
* @author HiF
* @date 2023/10/24 21:53
*/
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@MapperScan("com.heifan.demo.dao")
public class DruidConfig {

@Bean(name = DataSourceConstant.MASTER)
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource()
{
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return dataSource;
}

@Bean
@Primary
public DynamicDataSource dynamicDataSource()
{
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put(DataSourceConstant.MASTER,masterDataSource());
//设置动态数据源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
dynamicDataSource.setTargetDataSources(dataSourceMap);
//将数据源信息备份在defineTargetDataSources中
dynamicDataSource.setDefineTargetDataSources(dataSourceMap);
return dynamicDataSource;
}

}
  • DataSourceUtil # 数据源工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 数据源工具类
* @author HiF
* @date 2023/10/24 21:54
*/
@Slf4j
@Component
public class DataSourceUtil {
@Resource
DynamicDataSource dynamicDataSource;

/**
* 根据传递的数据源信息测试数据库连接
*/
public DruidDataSource createDataSourceConnection(DataSourceInfo dataSourceInfo) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl(dataSourceInfo.getUrl());
druidDataSource.setUsername(dataSourceInfo.getUserName());
druidDataSource.setPassword(dataSourceInfo.getPassword());
druidDataSource.setBreakAfterAcquireFailure(true);
druidDataSource.setConnectionErrorRetryAttempts(0);
try {
druidDataSource.getConnection(2000);
log.info("数据源连接成功");
return druidDataSource;
} catch (SQLException throwables) {
log.error("数据源 {} 连接失败,用户名:{},密码 {}",dataSourceInfo.getUrl(),dataSourceInfo.getUserName(),dataSourceInfo.getPassword());
return null;
}
}

/**
* 将新增的数据源加入到备份数据源map
*/
public void addDefineDynamicDataSource(DruidDataSource druidDataSource, String dataSourceName){
Map<Object, Object> defineTargetDataSources = dynamicDataSource.getDefineTargetDataSources();
defineTargetDataSources.put(dataSourceName, druidDataSource);
dynamicDataSource.setTargetDataSources(defineTargetDataSources);
dynamicDataSource.afterPropertiesSet();
}
}

注意事项

  1. DruidConfig类需要加上**@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})** ,不然项目启动会报错循环依赖。

  2. DruidConfig类加上**@MapperScan(“com.heifan.demo.dao”)** ,启动类就不需要写了。

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Slf4j
@RestController
public class DynamicController {

@Resource
DataSourceUtil dataSourceUtil;

@Resource
UrlClDao urlClDao;

@GetMapping("/test")
public Map<String, Object> dynamicDataSourceTest(){
Map<String, Object> map = new HashMap<>();
// 主库查询
List<UrlCl> urlCls = urlClDao.selectList(new QueryWrapper<>());
urlCls.forEach(item -> {
log.info("链接:{}",item.getUrlStr());
});
//在从库中查询
DynamicDataSourceHolder.setDynamicDataSourceKey(DataSourceConstant.DB_2);
DataSourceInfo dataSourceInfo = new DataSourceInfo();
dataSourceInfo.setUrl("jdbc:mysql://sql.xxxx.xxxxx.com:3306/xxxx?useSSL=false&useUnicode=true&autoReconnect=true&failOverReadOnly=false");
dataSourceInfo.setUserName("xxxxx");
dataSourceInfo.setPassword("xxxxx");
dataSourceInfo.setKeyCode("test");
map.put("dataSource",dataSourceInfo);
log.info("数据源信息:{}",dataSourceInfo);
//测试数据源连接
DruidDataSource druidDataSource = dataSourceUtil.createDataSourceConnection(dataSourceInfo);
if (Objects.nonNull(druidDataSource)){
//将新的数据源连接添加到目标数据源map中
dataSourceUtil.addDefineDynamicDataSource(druidDataSource,dataSourceInfo.getKeyCode());
//设置当前线程数据源名称-----代码形式
DynamicDataSourceHolder.setDynamicDataSourceKey(dataSourceInfo.getKeyCode());
//在新的数据源中查询用户信息
urlCls = urlClDao.selectList(new QueryWrapper<>());
urlCls.forEach(item -> {
log.info("从表链接:{}",item.getUrlStr());
});
//关闭数据源连接
druidDataSource.close();
}
return map;
}
}

运行结果

可以看到主库和从库查到的信息不一样,切换数据源成功,测试成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2023-10-24 21:40:32,785 INFO i.u.servlet [ServletContextImpl.java : 371] [] Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-10-24 21:40:32,785 INFO o.s.w.s.DispatcherServlet [FrameworkServlet.java : 525] [] Initializing Servlet 'dispatcherServlet'
2023-10-24 21:40:32,786 INFO o.s.w.s.DispatcherServlet [FrameworkServlet.java : 547] [] Completed initialization in 1 ms
2023-10-24 21:40:32,938 INFO c.a.d.p.DruidDataSource [DruidDataSource.java : 998] [] {dataSource-1} inited
2023-10-24 21:40:33,296 DEBUG c.h.d.d.U.selectList [BaseJdbcLogger.java : 137] [] ==> Preparing: SELECT id,url_name,url_str,create_time,modify_time,is_enable FROM url_cl
2023-10-24 21:40:33,316 DEBUG c.h.d.d.U.selectList [BaseJdbcLogger.java : 137] [] ==> Parameters:
2023-10-24 21:40:33,347 DEBUG c.h.d.d.U.selectList [BaseJdbcLogger.java : 137] [] <== Total: 3
2023-10-24 21:40:33,349 INFO c.h.d.c.DynamicController [DynamicController.java : 37] [] 链接:xxxx1.xyz
2023-10-24 21:40:33,349 INFO c.h.d.c.DynamicController [DynamicController.java : 37] [] 链接:xxxx2.xyz
2023-10-24 21:40:33,350 INFO c.h.d.c.DynamicController [DynamicController.java : 37] [] 链接:xxxx3.xyz
2023-10-24 21:40:33,350 INFO c.h.d.d.DynamicDataSourceHolder [DynamicDataSourceHolder.java : 23] [] 数据源切换为:db2
2023-10-24 21:40:33,350 INFO c.h.d.c.DynamicController [DynamicController.java : 47] [] 数据源信息:DataSourceInfo(keyCode=test, url=jdbc:mysql://sql.xxxx.xxxx.com:3306/xxxx?useSSL=false&useUnicode=true&autoReconnect=true&failOverReadOnly=false, userName=xxxx, password=xxxx)
2023-10-24 21:40:33,352 INFO c.a.d.p.DruidDataSource [DruidDataSource.java : 998] [] {dataSource-2} inited
2023-10-24 21:40:33,906 INFO c.h.d.u.DataSourceUtil [DataSourceUtil.java : 32] [] 数据源连接成功
2023-10-24 21:40:33,907 INFO c.h.d.d.DynamicDataSourceHolder [DynamicDataSourceHolder.java : 23] [] 数据源切换为:test
2023-10-24 21:40:34,383 DEBUG c.h.d.d.U.selectList [BaseJdbcLogger.java : 137] [] ==> Preparing: SELECT id,url_name,url_str,create_time,modify_time,is_enable FROM url_cl
2023-10-24 21:40:34,384 DEBUG c.h.d.d.U.selectList [BaseJdbcLogger.java : 137] [] ==> Parameters:
2023-10-24 21:40:34,427 DEBUG c.h.d.d.U.selectList [BaseJdbcLogger.java : 137] [] <== Total: 3
2023-10-24 21:40:34,428 INFO c.h.d.c.DynamicController [DynamicController.java : 58] [] 从表链接:hei.fan
2023-10-24 21:40:34,428 INFO c.h.d.c.DynamicController [DynamicController.java : 58] [] 从表链接:hei.fan
2023-10-24 21:40:34,428 INFO c.h.d.c.DynamicController [DynamicController.java : 58] [] 从表链接:hei.fan
2023-10-24 21:40:34,428 INFO c.a.d.p.DruidDataSource [DruidDataSource.java : 2071] [] {dataSource-2} closing ...
2023-10-24 21:40:34,437 INFO c.a.d.p.DruidDataSource [DruidDataSource.java : 2144] [] {dataSource-2} closed

总结

  • 参考了一些技术博客,很有帮助,感谢!
  • 不断试错才能成长,不能急躁,换个思路说不定会有更好的结果。
  • 基础实现了就能更好的根据业务进行拓展了,一定要注意代码质量。

SpringBoot实现多数据源,动态切换数据源
https://hif.icu/springboot_dynmic/
作者
HiF
发布于
2023年10月25日
许可协议