/saas-datasource-spring-boot-starter

A quick start tool for dynamic adding and switching data sources of SaaS multi-tenancy.

Primary LanguageJavaApache License 2.0Apache-2.0

saas-datasource-spring-boot-starter

一个支持SaaS多租户动态添加和切换数据源的快速启动工具。

在了解完如何使用后请务必仔细阅读注意事项最佳实践

完整解决方案现已重磅发布!请前往 Airboot-SaaS-DataSource

简单示例项目请前往 saas-datasource-samples

目前1.x版本主要支持Druid,后续会考虑增加对HikariCpBeeCpDbcp2的支持。


介绍

适用场景

saas-datasource-spring-boot-starter(以下简称“本工具”)适用于SaaS场景中 共享数据源,独立Schema独立数据源 的多租户架构,支持多种方式自动或手动切换租户数据源,并可在运行时动态添加租户数据源,使用轻量,简单方便。

注意,本工具并不适用于 共享Schema,共享数据表 的SaaS多租户架构(即租户仅在表中用tenantId来区分),如果想采用此架构,可参考或直接使用 Airboot-SaaS ,是基于Mybatis-Plus的一套完整解决方案。

数据源是一个较抽象的概念,比如一个共享VIP的数据库集群算一个数据源,一个独立的数据库服务器,一个服务器上的Mysql或Oracle服务实例,甚至一个数据服务中的独立Schema,都可以称为一个数据源,这取决于架构设计时所考虑的切分粒度,具体业务具体分析。

现在为方便起见,我们假设一个数据源指的是一台数据服务器,那么上述提到的三种SaaS架构的主要区别如下:

独立数据源 共享数据源,独立Schema 共享Schema,共享数据表
特性 每个租户都有自己独立的数据服务器,相互之间完全隔离 每一台数据服务器上都存在数量不定的多个租户,每个租户拥有自己的Schema,Schema之间完全隔离 所有租户都在一台数据服务器上的一个Schema中,仅通过数据表内的tenantId来做租户区分
优点 拥有最高的隔离性、安全性和性能 具备一定的隔离性和安全性,成本适中,性能较高,扩展方便 成本最低,设计简单,全局数据统计方便
缺点 成本太高,全局数据统计不方便 一个数据服务器有问题会影响到多个租户,全局数据统计不方便 隔离性和安全性最低,编码时必须严格注意tenantId,如有误操作很容易影响大片租户,随着租户数据量增加性能容易到达瓶颈
适用场景 混合云,对隔离性和安全性要求较高的租户,土豪 比较适中的方案,适合大部分SaaS场景,但在全局数据统计上要自己进行架构设计 适合低成本的小型项目,对隔离性和安全性要求不高,在可预见的未来数据量不大

本工具兼容 共享数据源,独立Schema独立数据源 两种架构(本质上取决于你提供什么样的jdbcUrl),使用本工具后,通常情况下开发者无需关心租户切换或tenantId等问题,在开发体验上与单租户(即非SaaS)开发无异。

请根据自身产品的业务特点及架构选型决定是否使用本工具。

版本对应说明

本工具基于 dynamic-datasource-spring-boot-starterDruid数据库连接池开发,可整合 Mybatis-PlusMybatis ,由于这些开源项目也在不断更新中,尤其像dynamic-datasource-spring-boot-starter这几年经历过数次大小重构,因此本工具需要针对其不同版本做出适配。

为了兼容可能存在的老旧项目,本工具在起始版本会对应dynamic-datasource-spring-boot-starter较早期的版本,而后续更新中会逐步对应不同的版本区间,使用本工具的开发者请务必确认好当前项目中这几个jar包所对应的版本区间,具体对应关系如下:

saas-datasource-spring-boot-starter dynamic-datasource-spring-boot-starter mybatis-plus-boot-starter mybatis-spring-boot-starter
1.5.0 & 1.4.0 version in (3.4.1, 3.5.1 (latest)] version <= 3.5.1 (latest) version <= 2.2.2 (latest)
1.3.0 version in (3.1.1, 3.4.1] version <= 3.5.1 (latest) version <= 2.2.2 (latest)
1.2.0 version in (2.4.2, 3.1.1] version <= 3.5.1 (latest) version <= 2.2.2 (latest)
1.1.0 & 1.0.0 version <= 2.4.2
根据@SaaS注解的位置分为两种情况:
1. 如果注解在Mapper上,则 version <= 3.0.7.1,若高于此版本dynamic-datasource会报错;
2. 如果注解不在Mapper上,则可使用目前最新版本 version <= 3.5.1 (latest)。
最佳实践,推荐上述第二种情况,注解不要放在Mapper上。
version <= 2.2.2 (latest)

快速使用

引入依赖

<dependency>
    <groupId>com.air-software</groupId>
    <artifactId>saas-datasource-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

配置默认数据源

通常情况下,我们会将租户数据源配置保存在一个公共库里,不必担心在切换数据源时会频繁查库,因为本工具会将已获取过的数据源缓存起来,如果切换时缓存中没有对应数据源,才会查库(具体看你的SaaSDataSourceProvider怎么实现)。

当然,有时我们也需要对数据源缓存池进行管理,关于这方面参见 管理数据源缓存池

因此,项目启动时的默认数据源推荐配置为公共库,此处的配置风格参照dynamic-datasource-spring-boot-starter,举例如下:

spring:
  datasource:
    dynamic:
      primary: common
      datasource:
        common:
          url: jdbc:mysql://localhost/saas_common?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true&autoReconnectForPools=true&allowMultiQueries=true&allowPublicKeyRetrieval=true
          username: root
          password: 123456
      druid:
        initial-size: 5
        min-idle: 10
        max-active: 20
        max-wait: 60000
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000
        max-evictable-idle-time-millis: 900000
        validation-query: select 1 from dual
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false

注意,如果你使用的dynamic-datasource-spring-boot-starter版本在3.3.3以下,则仍需要手动排除DruidDataSourceAutoConfigure。你可以在@SpringBootApplication注解中exclude,或者在配置文件中添加:

spring:
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure

实现数据源提供者

开发者需自行实现SaaS多租户数据源提供者,一个简单的实现示例如下:

import com.airsoftware.saas.datasource.core.SaaSDataSourceCreator;
import com.airsoftware.saas.datasource.provider.SaaSDataSourceProvider;
import com.airsoftware.saas.datasource.sample.entity.DataSourceConfig;
import com.airsoftware.saas.datasource.sample.mapper.DataSourceConfigMapper;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;


@Component
public class MySaaSDataSourceProvider implements SaaSDataSourceProvider {
    
    @Resource
    private DataSourceConfigMapper dataSourceConfigMapper;
    
    @Resource
    private SaaSDataSourceCreator saasDataSourceCreator;
    
    public static String JDBC_URL_PREFIX = "jdbc:mysql://";
    
    public static String JDBC_URL_SUFFIX = "?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true&autoReconnectForPools=true&allowMultiQueries=true&allowPublicKeyRetrieval=true";
    
    @Override
    public DataSource createDataSource(String dsKey) {
        DataSourceConfig dataSourceConfig = dataSourceConfigMapper.selectById(dsKey);
        String jdbcUrl = JDBC_URL_PREFIX + dataSourceConfig.getHost() + "/" + dataSourceConfig.getSchemaName() + JDBC_URL_SUFFIX;
        
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        dataSourceProperty.setUrl(jdbcUrl);
        dataSourceProperty.setUsername(dataSourceConfig.getUsername());
        dataSourceProperty.setPassword(dataSourceConfig.getPassword());
        dataSourceProperty.setPoolName(dsKey);
        
        return saasDataSourceCreator.createDruidDataSource(dataSourceProperty);
    }
    
}

注意,为保证数据源提供者查询数据源时能够正确访问到公共库,而不受其他已切换数据源的影响,建议为查询数据源的Mapper添加dynamic-datasource-spring-boot-starter自带的@DS注解,强制使用公共库:

@DS("common")
public interface DataSourceConfigMapper

另外如果你使用的是1.1.0及以上版本,也可以直接使用SaaSDataSource.switchTo来强制切换至公共库,记得切换后立即调用clearCurrent清理一下:

SaaSDataSource.switchTo("common");
DataSourceConfig dataSourceConfig = dataSourceConfigMapper.selectById(dsKey);
SaaSDataSource.clearCurrent();

不建议在1.0.0采用此方法,因为1.0.0版本的SaaSDataSource.switchTo方法并未被优化。

启用注解

在SpringBoot主启动类上添加@EnableSaaSDataSource注解,表示启用SaaS数据源功能。

@SpringBootApplication
@EnableSaaSDataSource
public class SaaSApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(SaaSApplication.class, args);
    }
}

如果你想使用Request Session或Header的方式切换数据源,则在需要切换数据源的类或方法上标记@SaaS注解,使用此注解时必须显式指定租户标识字段名称。举例如下:

@SaaS("tenantId")
@RestController
@RequestMapping("/user")
public class UserController {

    ......
    ......

}

比如我在使用注解时设置为@SaaS("tenantId"),那么我在Request Session或Header中就需要用tenantId字段来设置租户标识,而这个租户标识在首次切换至此租户时,会传递至我自己实现的SaaSDataSourceProvider中,以此来获取租户对应数据源。

切换数据源

本工具共提供三种方式来切换数据源,按优先级从高到底排列如下:

  • SaaSDataSource.switchTo(String/Long/Integer dsKey)
  • Request Session
  • Request Header

你可以在任意地方多次调用SaaSDataSource.switchTo来手动切换数据源,他会影响到你下一次即将执行的数据库操作,常用于拦截器、定时任务、异步操作、循环刷库,跨库统计、消息消费等场景。

SaaSDataSource内部使用了栈来存储多次切换的数据源,switchTo方法入栈,current方法获取当前栈顶数据,clearCurrent方法会让当前栈顶数据出栈,clearAll方法会清理整个栈。

建议在每次手动切换完成,且对应数据源的业务处理完成后,调用clearCurrent来清理刚刚手动切换的数据源,以避免对后续流程产生影响。或者也可以在整个业务流程的最后调用clearAll

注意:在1.1.0及以上版本,如果你只使用SaaSDataSource来手动切换数据库,则不需要再标记@SaaS注解,即使标记了注解,注解中的值也会被忽略。

而在1.0.0版本,则仍需要在调用SaaSDataSource.switchTo之后的流程中标记@SaaS注解,即必须在注解生效前执行SaaSDataSource.switchTo

管理数据源缓存池(1.5.0+)

每个数据源都只会在第一次切换时调用到你自己实现的SaaSDataSourceProvider,之后就会被缓存。

假设你的数据源配置存在数据库中,你修改了库中的某个数据源配置,但是这个配置已经被缓存了。此时如果你想在不重新启动服务的情况下让此数据源使用到最新的配置,可以使用SaaSDataSourcePool.remove(dsKey)方法来移除数据源缓存池中的指定数据源。那么下一次再切换至此数据源时就会再次调用你自己实现的SaaSDataSourceProvider,获取到最新的配置。

SaaSDataSourcePool就是用来管理数据源缓存池的工具,包含了多种操作方法,注意它仅在1.5.0以上版本可用。


注意事项

以下注意事项非常重要,请开发者务必仔细阅读:

  • 公共库中的表最好不要跟租户库中的业务表有重合,因为当切换数据源失败时,会自动退回至最近一次切换生效的数据源。如果此前未做过任何方式的切换,则退回至应用启动时配置的默认数据源(通常为公共库)。此时若公共库中存在同名业务表的话,那在明面上是不会报错的,只不过数据都到公共库里了,这样不利于排查问题。
  • 为安全起见,尽量不要使用Header模式,因为前端传递的数据永远是不可信的。如果要使用前端直接传递的值,一定要配合权限控制,比如整个系统的超级管理员想要自由切换至不同租户,此时就需要使用前端传值。这也是我保留了Header模式,但优先级降为最低的原因。
  • 事务中无法切换数据源,强行切换可能会导致异常。首先一定要注意@SaaS的标记位置,至少应在最外层事务或更上一层的调用方标记此注解,即保证注解在事务开启前发挥作用,以切换到正确的数据源。其次不要在事务内调用SaaSDataSource.switchTo,而应在事务开启前调用。如果没有在事务开启前通过注解或手动切换至正确的数据源,则事务会在默认数据源上执行。
  • 本工具不提供分布式事务的实现,也未做过相关测试,如果需要分布式事务请开发者自行实现和测试,理论上本工具兼容分布式事务。
  • 在定时任务、异步操作、消息消费等无法获取Request上下文的场景下,一定要记得处理业务前调用SaaSDataSource.switchTo来手动切换至想要操作的数据源
  • spring-boot2.6.x版本后默认禁止了循环依赖,这可能会导致使用本工具时启动报错,我在1.4.0版本中修复了此问题。如果你使用的是1.3.0及以下版本,可在配置文件中显式指定:
spring:
  main:
    allow-circular-references: true

或在实现数据源提供者SaaSDataSourceProvider时,将SaaSDataSourceCreator标记为懒加载:

@Lazy
@Resource
private SaaSDataSourceCreator saasDataSourceCreator;

最佳实践

基于上述注意事项,结合现代Web开发的技术倾向,可以得出以下几条最佳实践:

  • 为保障注解在事务开启前发挥作用,在Web项目中推荐将@SaaS标记在Controller,一般这就是事务的顶层了。大部分项目中都会有一个BaseController作为所有Controller的父类,将@SaaS注解标记在父类上,对所有Controller都会起作用。
  • 现代Web项目中使用Token的情况已逐步超过Session,在Token场景下,我们可以将dsKey放入Token中,或为安全起见将dsKey放入Redis,而Redis Key放入Token中。随后我们在拦截器中解析Token之后,使用获得的dsKey调用SaaSDataSource.switchTo来切换数据源,这样在编写业务代码时就无需关心租户切换问题了,最后不要忘了在拦截器的afterCompletion中调用SaaSDataSource.clearAll方法(1.0.0版本是SaaSDataSource.clear)。

更新日志

1.5.0

  • 新增数据源池工具类SaaSDataSourcePool,用于管理已被缓存至池中的数据源,可能的使用场景参见 管理数据源缓存池
  • 更新并适配druid-spring-boot-starter1.2.9版本(目前最新版);
  • 更新并适配spring-boot-starter-web2.6.7版本(目前最新版),但pom scope为provided,即最终以你项目中实际使用的spring-boot版本为准。

1.4.0

  • 更新并适配dynamic-datasource-spring-boot-starter3.5.1版本(目前最新版);
  • 更新并适配druid-spring-boot-starter1.2.8版本;
  • 更新并适配spring-boot-starter-web2.6.5版本,但pom scope为provided,即最终以你项目中实际使用的spring-boot版本为准;
  • 解决可能存在的循环依赖问题,具体细节参见注意事项
  • 去掉了@SaaS注解的默认值,现在使用此注解时必须显式指定租户标识字段名称

1.3.0

  • 更新并适配dynamic-datasource-spring-boot-starter3.4.1版本;
  • 更新并适配spring-boot-starter-web2.1.1.RELEASE版本,但pom scope为provided,即最终以你项目中实际使用的spring-boot版本为准;
  • 新增SaaSDataSourceClassResolver来解析注解标记的类,原因是dynamic-datasource-spring-boot-starter3.1.1版本后删除了本工具之前使用的对应API,所以只能本工具自己再实现一个;
  • 支持SPI,开发者可以省略driverClassName配置了。

1.2.0

  • 更新并适配dynamic-datasource-spring-boot-starter3.1.1版本;
  • 优化了SaaSDataSource,底层改为使用ArrayDeque来实现栈;
  • 增加SaaSDataSource.removeAll方法,可强制移除所有数据源,包含DynamicDataSource上下文中的数据源。如果你不确定业务流程完成后是否还有残留数据,可在最后(比如拦截器的afterCompletion中)调用此方法来确保移除。

1.1.0

  • 优化了SaaSDataSource,现在可以随时强制切换数据源,不再依赖@SaaS注解;
  • 增加了数据源管理器,优化内部拦截器代码。

1.0.0

  • 支持Request Session和Header切换数据源;
  • 支持SaaSDataSource手动切换数据源,但需要在切换后的调用流程中存在@SaaS注解标记来触发。