关于(历史)系统多租户设计的后续

相关文章:

因为某些原因,临近上线前我们调整了方案,即使用多数据源的方案去对系统进行多租户改造,这也是《基于 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 的,从而配置多个 basePackagessqlSessionFactoryRef。但是在多租户的系统中其实 Mapper 只有一套,所以这个对应关系可以改一下,要弱化这里 DataSourceName 的概念:

在这里插入图片描述

即所有租户共用 MapperScan 等配置,所有的数据源都是同一级的。那么到底走哪个数据源呢,需要将当前环境设置到 ThreadLocal 中,然后 AbstractRoutingDataSource 再基于当前环境和读写分离注解去选择数据源。

定时任务

定时任务我觉得这块也没有设计的很好,后续会再改进。在《基于 MyBatis 实现多租户数据隔离的实践》也做了相关介绍,系统代码中所有的数据已经有租户标识 region 去数据隔离了。

循环所有租户,每次循环将 region 租户标识参数放入当前循环中,租户过多可以拆分多个定时任务;

这个方案在多数据源方案中是不行的,因为多数据源最关键的是多个库,也就是说一个定时任务需要跑多个库,即系统环境设置级别是高于合并数据方案的设置级别的。所以需要循环所有环境,每次循环在 ThreadLocal 中设置环境。

欢迎关注公众号
​​​​​​在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值