相关文章:
因为某些原因,临近上线前我们调整了方案,即使用多数据源的方案去对系统进行多租户改造,这也是《基于 MyBatis 实现多租户数据隔离的实践》中与各位伙伴讨论的相对好的方案。这样改造过程平滑,两种方案(数据合并方案和多数据源方案)的风险、操作难度不在一个数量级。
虽然多数据源方案相对简单很多,但还是要注意一些问题。这里将一些问题记录一下。
(历史)系统多数据源配置
AbstractRoutingDataSource
其实在 Spring/Spring Boot 中多数据源并不是什么麻烦的事情,一般项目都是使用的 AbstractRoutingDataSource
进行多数据源控制。但是历史系统都有一个问题就是“注释很少、会有一定程度的封装”,造成很多功能在改造的时候会有难度。
所以这部分在改造的时候需要把握 AbstractRoutingDataSource
的核心方法。其实这个类就是一个模版类。关键要注意这个方法:
protected abstract Object determineCurrentLookupKey();
很明显需要子类去实现。方法名中有“Key”,在 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineTargetDataSource
方法中:
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)) {
//设置默认数据源,根据 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#setDefaultTargetDataSource 设置
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
可以看到,resolvedDataSources
就是根据 determineCurrentLookupKey
方法返回的 Key 去获取数据源。那么 resolvedDataSources
是从哪来的呢:
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(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);
}
}
可以看到 resolvedDataSources
的数据来自于 targetDataSources
,而 AbstractRoutingDataSource
也给我们提供了 setTargetDataSources
方法:
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
所以我们可以在子类中将数据源初始化好之后设置到 targetDataSources
即可。
关于多数据源的概念和 @MapperScan
多数据源最直白的解释就是一个数据库就是一个数据源,但是我这个项目由于历史设计原因,可能会有理解上的误导,比如在我这个项目中有一个 DataSourceName 的概念,但是这个 DataSourceName 是跟租户名称挂钩的,即:
再结合相关的配置就成了这样的对应关系:
要注意的是我们是可以配置多个 MapperScan
的,从而配置多个 basePackages
、sqlSessionFactoryRef
。但是在多租户的系统中其实 Mapper
只有一套,所以这个对应关系可以改一下,要弱化这里 DataSourceName 的概念:
即所有租户共用 MapperScan
等配置,所有的数据源都是同一级的。那么到底走哪个数据源呢,需要将当前环境设置到 ThreadLocal
中,然后 AbstractRoutingDataSource
再基于当前环境和读写分离注解去选择数据源。
定时任务
定时任务我觉得这块也没有设计的很好,后续会再改进。在《基于 MyBatis 实现多租户数据隔离的实践》也做了相关介绍,系统代码中所有的数据已经有租户标识 region
去数据隔离了。
循环所有租户,每次循环将
region
租户标识参数放入当前循环中,租户过多可以拆分多个定时任务;
这个方案在多数据源方案中是不行的,因为多数据源最关键的是多个库,也就是说一个定时任务需要跑多个库,即系统环境设置级别是高于合并数据方案的设置级别的。所以需要循环所有环境,每次循环在 ThreadLocal
中设置环境。
欢迎关注公众号