Spring Boot JPA MySQL 多租户系统 Part4 - 版本管理

前言

在上篇文章中,我们使用 MasterDataSource 管理租户信息,使用 TenantDataSource 连接数据库处理具体业务逻辑。完成了前端租户管理的基础,无需手动配置租户信息,和重启应用程序。

上篇:Spring Boot JPA MySQL 多租户系统 Part3 - 管理租户

本篇是对之前系列文章的调整、补充和完善,讨论的内容有:

  1. 多线程中选择租户的方法。
  2. 数据库和数据表生成方法的调整。
  3. 数据库版本管理。

本文开发环境参考之前系列文章。

多线程

异步任务

我们知道,每个请求的处理对应一个新的线程。但是,请求处理(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

自动建表

思路是这样的:

Entities
Changelog
Tenant1 Tables
Tenant2 Tables
Tenant... Tables

通过 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 删除掉。
remove-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 库,可以根据标签获取不同部分对应的源码。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PeterGamp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值