首先,我们先了解一下动态数据源路由的概念,即:能够将注入的DataSource根据租户使用的不同而使用不同的数据源,同时可根据租户的信息动态的增减数据源,并且还能实时生效。
如何设计一个动态数据源
spring是如何管理动态数据源
- 在application.yml中配置一个多数据源,类似于下图:
- 配置一个注解的bean,把配置文件中的内容读取出来,类似于下图:
- 根据不同的请求,分别去请求master和slave
那我们思考一个问题,以上方式有什么缺陷吗?
- 数据源只能写在文件中,每次修改都需要重启。
- 注解的bean代码也是写死的,修改配置文件的同时也要修改代码,并且还需要重启。
那我们如何解决以上的问题呢?
数据源配置信息存储
把从配置文件存储,改成DB存储或配置中心存储都可以,这样的话我们就可以动态的改变数据源的配置信息,并且能被程序捕捉到。
如何灵活的不用重启服务的方式更新数据源呢?
首先,我们需要看看,spring的动态数据源的原理是什么?
先看看入口类:DynamicDataSource,继承了父类AbstractRoutingDataSource,父类继承了InitializingBean接口,所以在启动项目时,会去进行初始化动作,把配在yaml中的配置数据源信息加载进来,并且在AbstractRoutingDataSource中,存放了关键的几个Map对象,存放了dataSources相关的内容。
启动方法后,会将配置的多个数据源信息,统统放到这里,通过getConnection()方法对外提供数据源。
这种方式上是否存在一定缺陷?
AbstractRoutingDataSource关键源码分析
先看看UML图
从UML图中,我们可以看到,是实现了DataSource接口,所以可以做各种DB查询的扩展。
再看看关键源码
public void afterPropertiesSet() {
// 判断目标数据源是否为空
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
// 先把实际对外调用的source置空
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
// 而后遍历目标数据源,把目标数据源的数据put到实际对外调用的数据源中。
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
// 获取数据源
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 获取key
Object lookupKey = this.determineCurrentLookupKey();
// 从resolvedDataSources 获取datasource
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();
以上有两个问题:
- 只能项目启动时初始化;
- 在于
this.resolvedDataSources = CollectionUtils.newHashMap方法会重新new一个Map类出来,这就会导致如果是在并发度较高的情况下,会发生找不到数据源的问题;或者正在查询的时候,发起了删除,产生不安全的隐患。
如何解决源码中的问题
另起炉灶
UML设计图
AbstractDynamicDataSource
// 构造底层map 使用ConcurrentHashMap代替普通的HashMap
protected Map<Object, T> resolvedDataSources = new ConcurrentHashMap<>();
protected abstract void resolveSpecifiedDataSource(Object lookupKey, Object dataSource) throws IllegalArgumentException;
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
// 替换实际数据源 这里直接替换,不需要再重新new一个Map
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
resolveSpecifiedDataSource(lookupKey, value);
});
}
@Override
protected void resolveSpecifiedDataSource(Object lookupKey, Object dataSource) throws IllegalArgumentException {
// 判断删除标记,并关闭老的source,并删除数据源标记
if (dataSource instanceof Integer && ((int) dataSource) == GenericDataSource.DELETE_FLAG) {
DataSource source = resolvedDataSources.remove(lookupKey);
close(source);
removeLookupKey(lookupKey);
}
// 变更,并关闭老的source,新增数据源标记
else {
DataSource curSource = resolveSpecifiedDataSource(dataSource);
DataSource oldSource = resolvedDataSources.get(lookupKey);
resolvedDataSources.put(lookupKey, curSource);
close(oldSource);
addLookupKey(lookupKey);
}
}
protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
if (dataSource instanceof DataSource) {
return (DataSource) dataSource;
} else if (dataSource instanceof String) {
return dataSourceLookup.getDataSource((String) dataSource);
} else {
throw new IllegalArgumentException( "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
}
}
这样就能完美解决,因数据源变更导致的问题。
那如何解决动态问题呢
很多方案,比如,简单一点的就是定时查询DB的变更,然后发起对动态数据源的维护; 复杂一点,可以实时监听,这里就不赘述。