Spring 动态切换、添加数据源实现以及源码浅析

公司项目需求,由于要兼容老系统的数据库结构,需要搭建一个 可以动态切换、添加数据源的后端服务。

分析

参考了过去的项目,通过配置多个SqlSessionFactory 来实现多数据源,这么做的话,未免过于笨重,而且无法实现动态添加数据源这个需求

通过 spring AbstractRoutingDataSource 为我们抽象了一个 DynamicDataSource 解决这一问题

源码

简单分析下 AbstractRoutingDataSource 的源码

targetDataSources 就是我们的多个数据源,在初始化的时候会调用afterPropertiesSet(),去解析我们的数据源 然后 put 到 resolvedDataSources

实现了 DataSource 的 getConnection(); 我们看看 determineTargetDataSource(); 做了什么

通过下面的 determineCurrentLookupKey();(这个方法需要我们实现) 返回一个key,然后从 resolvedDataSources (其实也就是 targetDataSources) 中 get 一个数据源,实现了每次调用 getConnection(); 打开连接 切换数据源,如果想动态添加的话 只需要重新 set targetDataSources 再调用 afterPropertiesSet() 即可

Talk is cheap. Show me the code

我使用的springboot版本为 1.5.x,下面是核心代码
完整代码:https://gitee.com/yintianwen7/spring-dynamic-datasource

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
/**
* 多数据源配置
*
* @author Taven
*
*/
@Configuration
@MapperScan("com.gitee.taven.mapper")
public class DataSourceConfigurer {

/**
* DataSource 自动配置并注册
*
* @return data source
*/
@Bean("db0")
@Primary
@ConfigurationProperties(prefix = "datasource.db0")
public DataSource dataSource0() {
return DruidDataSourceBuilder.create().build();
}

/**
* DataSource 自动配置并注册
*
* @return data source
*/
@Bean("db1")
@ConfigurationProperties(prefix = "datasource.db1")
public DataSource dataSource1() {
return DruidDataSourceBuilder.create().build();
}

/**
* 注册动态数据源
*
* @return
*/
@Bean("dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("dynamic_db0", dataSource0());
dataSourceMap.put("dynamic_db1", dataSource1());
dynamicRoutingDataSource.setDefaultTargetDataSource(dataSource0());// 设置默认数据源
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
return dynamicRoutingDataSource;
}

/**
* Sql session factory bean.
* Here to config datasource for SqlSessionFactory
* <p>
* You need to add @{@code @ConfigurationProperties(prefix = "mybatis")}, if you are using *.xml file,
* the {@code 'mybatis.type-aliases-package'} and {@code 'mybatis.mapper-locations'} should be set in
* {@code 'application.properties'} file, or there will appear invalid bond statement exception
*
* @return the sql session factory bean
*/
@Bean
@ConfigurationProperties(prefix = "mybatis")
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 必须将动态数据源添加到 sqlSessionFactoryBean
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
return sqlSessionFactoryBean;
}

/**
* 事务管理器
*
* @return the platform transaction manager
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
}

通过 ThreadLocal 获取线程安全的数据源 key

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
package com.gitee.taven.config;

public class DynamicDataSourceContextHolder {

private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "dynamic_db0";
}
};

/**
* To switch DataSource
*
* @param key the key
*/
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}

/**
* Get current DataSource
*
* @return data source key
*/
public static String getDataSourceKey() {
return contextHolder.get();
}

/**
* To set DataSource as default
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}

}

动态 添加、切换数据源

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
/**
* 动态数据源
*
* @author Taven
*
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

private final Logger logger = LoggerFactory.getLogger(getClass());

private static Map<Object, Object> targetDataSources = new HashMap<>();

/**
* 设置当前数据源
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
logger.info("Current DataSource is [{}]", DynamicDataSourceContextHolder.getDataSourceKey());
return DynamicDataSourceContextHolder.getDataSourceKey();
}

@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
DynamicRoutingDataSource.targetDataSources = targetDataSources;
}

/**
* 是否存在当前key的 DataSource
*
* @param key
* @return 存在返回 true, 不存在返回 false
*/
public static boolean isExistDataSource(String key) {
return targetDataSources.containsKey(key);
}

/**
* 动态增加数据源
*
* @param map 数据源属性
* @return
*/
public synchronized boolean addDataSource(Map<String, String> map) {
try {
Connection connection = null;
// 排除连接不上的错误
try {
Class.forName(map.get(DruidDataSourceFactory.PROP_DRIVERCLASSNAME));
connection = DriverManager.getConnection(
map.get(DruidDataSourceFactory.PROP_URL),
map.get(DruidDataSourceFactory.PROP_USERNAME),
map.get(DruidDataSourceFactory.PROP_PASSWORD));
System.out.println(connection.isClosed());
} catch (Exception e) {
return false;
} finally {
if (connection != null && !connection.isClosed())
connection.close();
}
String database = map.get("database");//获取要添加的数据库名
if (StringUtils.isBlank(database)) return false;
if (DynamicRoutingDataSource.isExistDataSource(database)) return true;
DruidDataSource druidDataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(map);
druidDataSource.init();
Map<Object, Object> targetMap = DynamicRoutingDataSource.targetDataSources;
targetMap.put(database, druidDataSource);
// 当前 targetDataSources 与 父类 targetDataSources 为同一对象 所以不需要set
// this.setTargetDataSources(targetMap);
this.afterPropertiesSet();
logger.info("dataSource {} has been added", database);
} catch (Exception e) {
logger.error(e.getMessage());
return false;
}
return true;
}

}

可以通过 AOP 或者 手动 DynamicDataSourceContextHolder.setDataSourceKey(String key) 切换数据源

需要注意的:当我们开启了事务之后,是无法在去切换数据源的

本文项目源码:https://gitee.com/yintianwen7/spring-dynamic-datasource
参考文献:https://github.com/helloworlde/SpringBoot-DynamicDataSource