背景
在项目开发过程中,我们可能会遇到一个场景:某个类型数据源有多个数据源实例,需要我们按照不同的请求切换到不同数据源去。
而目前绝大多数java应用都是基于Spring框架来开发,我们很多时候相关的数据源连接都是交给了Spring框架去管理,这就需要Spring能够支持动态数据源切换。
方案
Spring中预留了这个接口,通过AbstractRoutingDataSource
能够动态切换数据源。
java
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
这是一个抽象类,预留了一个抽象方法:
java
protected abstract Object determineCurrentLookupKey();
我们知道,数据源一般都会提供一个getConnection
方法来获取一个连接,在AbstractRoutingDataSource
实现如下:
java
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
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;
}
可以看到,AbstractRoutingDataSource
获取连接的主要逻辑就是通过determineCurrentLookupKey
获取到一个数据源的关联key,然后从resolvedDataSources
中去获取。
而resolvedDataSources
的初始化,则放在afterPropertiesSet
中:
java
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
这里起始就是通过targetDataSources
中指定的数据源复制到resolvedDataSources
中去。因此如果多数源是固定的,那么只需要实现determineCurrentLookupKey
方法即可。但是如果多数据源不固定,比如可能会有数据源的变更,那么这种实现是不能够支持,因为这种实现从服务启动的视乎,后续数据源就不能发生变更,这需要我们自己实现determineTargetDataSource
.
下面是一个参考实现:
java
public class DataSourceContextHolder {
private static final ThreadLocal<String> DATASOURCE_CONTEXT_KEY_HOLDER = new ThreadLocal<>();
public static void switchDataSource(String key){
log.info("Switch to data source:" + key);
DATASOURCE_CONTEXT_KEY_HOLDER.set(key);
}
public static String getDataSourceKey(){
return DATASOURCE_CONTEXT_KEY_HOLDER.get() ;
}
}
public class DynamicDataSource extends AbstractRoutingDataSource {
private Map<Object, Object> targetDataSources = new HashMap<>();
private Map<Object, DataSource> dataSources = new HashMap<>();
public DynamicDataSource (){
super.setDefaultTargetDataSource(null);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected DataSource determineTargetDataSource() {
Object dataSourceKey = determineCurrentLookupKey();
return dataSources.get(dataSourceKey);
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder .getDataSourceKey();
}
public synchronized void addDataSource(String key, DataSource dataSource){
targetDataSources.put(key,dataSource);
dataSources.put(key,dataSource);
log.info("add tenant dynamic dataSource for tenantId = {} ",key);
}
}
这样我们通过DataSourceContextHolder
来调整当前线程关联的数据源。