前言
在上篇文章中,我们使用 MasterDataSource 管理租户信息,使用 TenantDataSource 连接数据库处理具体业务逻辑。完成了前端租户管理的基础,无需手动配置租户信息,和重启应用程序。
本篇是对之前系列文章的调整、补充和完善,讨论的内容有:
- 多线程中选择租户的方法。
- 数据库和数据表生成方法的调整。
- 数据库版本管理。
本文开发环境参考之前系列文章。
多线程
异步任务
我们知道,每个请求的处理对应一个新的线程。但是,请求处理(Controller、Service等)之外生成的对象也即应用程序主线程生成的对象,会在请求处理线程中共享。
比如:声明为 Object 的 TenantContext 会被请求线程共用。为了在一次请求中保持 currentTenant 变量值不变,或者说不被其他请求线程修改。我们使用了 ThreadLocal 来修饰它,这样每个线程都会生成一个 currentTenant 的副本,就不会相互影响了。
使用 ThreadLocal 修饰后,如果我们在请求线程中新开线程处理异步任务(@Async),也会有一个 currentTenant 副本,初始值为 null。这时如果在异步任务中有数据库读写操作,就会产生与期待不一致的结果。原因是,在异步任务中读取到的 currentTenant 值没有继承到请求处理线程的值。
一种方法是在配置异步线程池时,添加修饰器,让 currentTenant 值从请求线程中继承。
@Configuration
class AsyncConfig : AsyncConfigurerSupport() {
override fun getAsyncExecutor(): Executor {
val executor = ThreadPoolTaskExecutor()
executor.corePoolSize = 7
executor.maxPoolSize = 42
executor.queueCapacity = 11
executor.setThreadNamePrefix("TenantTaskExecutor-")
executor.setTaskDecorator { runnable ->
val tenant = TenantContext.getTenantId() // code 1
Runnable {
TenantContext.setTenantId(tenant) // code 2
runnable.run()
}
}
executor.initialize()
return executor
}
}
其中:“code 1” 会在请求线程中执行,"code 2"会在异步任务新开线程中执行。这样就继承了 currentTenant 值。
另一种方法是使用 InheritableThreadLocal 替换 ThreadLocal,在新开线程中使用 InheritableThreadLocal 定义的变量会继承原来的值,实现与上一种方法相同的效果。虽然 InheritableThreadLocal 可以在线程间继承,但是多个请求线程之间没有继承关系,它们之间 currentTenant 值依然相互独立。
定时任务
Spring Boot 定时任务(@Scheduled)也会新开线程来处理,默认也是没有配置租户信息的,也就是 currentTenant 值是 null。当我们需要在定时任务中操作数据库时,需要再次指定使用的租户数据库。
经常遇到的情况可能是对每个租户数据库都执行一遍定时任务。这里有如下简单示例。
在 TenantService 中添加下列方法:
fun runOnAllTenants(run: () -> Unit) {
TenantContext.setTenantId(tenantProperties.masterDb)
tenants.findAll().forEach {
TenantContext.setTenantId(it.dbName)
run()
}
}
在测试位置调用:
/**
* 每 1min 执行一次
*/
@Scheduled(cron = "30 * * * * ?")
fun testSchedule() {
Log.i(TAG, "start scheduling")
tenantService.runOnAllTenants {
persons.findAll()
}
Log.i(TAG, "end scheduling")
}
代码调整
距离发布上篇文章也有多日,知识有所更新。之前的记录可以作为另一种实现方案。本篇引入 Liquibase 来管理数据库版本,同时也可以简化数据表的创建过程。
Liquibase 使用请参考:Spring Boot 集成和使用 Liquibase
自动建库
Mysql 自动创建数据库其实可以很简单地实现,只需要在 jdbc url 配置位置添加 createDatabaseIfNotExist=true。完整 url :
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mm?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT&createDatabaseIfNotExist=true
自动建表
思路是这样的:
通过 JPA Buddy 将 Model (Enities) 转换成 DDL 日志文件 (Changelog),再通过 Liquibase Java API 读取日志文件生成数据表 (Tables)。Liquibase ChangeLog 文件是关键中间件,一次生成,到处使用。
我们需要分别为 MasterDataSource 和 TenantDataSource 添加日志文件。这里为MultiTenantProperties 扩展两个属性 tenantChangelog 、masterChangelog 分别指定这个两个日志文件的路径:
@Configuration
@ConfigurationProperties("multi-tenant")
class MultiTenantProperties(
var scanPackage: String = "",
var defaultDb: String = "",
var defaultGroup: String = "",
var tenantChangelog: String = "",
var masterDb: String = "",
var masterChangelog: String = ""
)
application.properties 文件相关配置示例:
#multitenancy
multi-tenant.scan-package=com.example.multitenant.model
multi-tenant.default-db=tenant1
multi-tenant.default-group=tenant1.tenant1.tenant1
multi-tenant.master-db=tenant_master
multi-tenant.master-changelog=classpath:db/master-changelog.sql
multi-tenant.tenant-changelog=classpath:db/tenant-changelog.sql
#liquibase
spring.liquibase.enabled=false
spring.liquibase.enabled 指定为 false 是为了关闭 SpringBoot 自动读取 changelog 文件建表的功能。这一功能只能关联配置好的 DataSource 更新数据库,在这里不再适用,我们需要手动使用 Liquibase Java API 更新数据库。
生成 Changelog
Liquibase 参考文章已经描述使用 JPA Buddy 生成 changelog 文件的方法,这里不再赘述。只是,这里将生成两个 changelog 文件。在生成时需要将不需要的 changeset 删除掉。
比如在生成 tenant-changelog 时需要删除 tenant 相关 changeset,这很好理解,因为业务数据库中不包含租户信息数据库,反过来亦然。
生成 Tables
有了 tenant-changelog 和 master-changelog,我们还需要读取它们,来生成对应的数据表。在 DataSources 中添加以下方法:
private fun runLiquibase(dataSource: DataSource, changelog: String) {
Log.i(TAG, "runLiquibase")
val connection = JdbcConnection(dataSource.connection)
val database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(connection)
val liquibase = liquibase.Liquibase(
changelog,
ClassLoaderResourceAccessor(),
database
)
liquibase.update(Contexts())
}
该方法先根据 DataSource 连接信息生成 Liquibase,然后在配置 changelog 文件路径后,将变更应用到数据库实现更新。
生成 MasterDataSource 和 TenantDataSource 的方法相应修改为:
@PostConstruct
private fun initMasterDataSource() {
val dbName = tenantProperties.masterDb
val jdbcUrl = replaceJdbcDb(dataSourceProperties.url, dbName)
val masterDataSource = createDatasource(jdbcUrl)
dataSources[tenantProperties.masterDb] = masterDataSource
runLiquibase(masterDataSource, tenantProperties.masterChangelog)
}
fun initTenantDataSources() {
tenants.findAll().forEach {
if (!dataSources.containsKey(it.dbName)) {
val jdbcUrl = replaceJdbcDb(dataSourceProperties.url, it.dbName)
val datasource = createDatasource(jdbcUrl)
dataSources[it.dbName] = datasource
runLiquibase(datasource, tenantProperties.tenantChangelog)
}
}
}
使用 Liquibase 生成的 Changelog ,帮我们省去了扫描 Entity 和生成数据表的几个方法,DataSources 代码简洁了许多。
版本管理
有了 Liquibase,数据库版本管理就相对简单了。当数据库结构变化时,生成 Diff Changelog 文件,重新配置下 changelog 文件路径即可更新原来的数据库。具体方法已经在 Liquibase 参考文章中描述。
总结
本文补充了上篇关于多线程的处理细节说明,然后在之前系列文章基础上,引入 Liquibase 简化数据库和数据表的生成代码,同时让多租户模块拥有了数据库版本管理的能力。
后续我们会探索将已经渐渐成形的多租户模块,从项目中解耦,变成独立的依赖。请点赞评论关注哦。
本文源码:https://gitee.com/yoshii_x/multi-tenant.git
系列文章使用同一个 Git 库,可以根据标签获取不同部分对应的源码。