安装与配置
Maven 依赖
<dependency>
<groupId>com.simperfect.commons</groupId>
<artifactId>simperfect-dynamic-datasource</artifactId>
</dependency>
配置项说明
多租户配置项说明
配置项前缀 simperfect.datasource.dynamic
属性名 | 类型 | 默认值 | 描述 |
---|---|---|---|
enabled | boolean | true | 是否启用多租户模式 |
autoUpdateEnabled | boolean | true | 是否启用多数据自动更新服务,一般程序都需要自动更新数据源。例如:租户管理台就不需要开启,因为它需要手动维护数据源。「仅在多租户模式下此参数才会生效」 |
datasourceInterceptorEnabled | boolean | true | 是否启用数据源拦截器,如果启用,则会通过 header 中的 tenant-sn 自动切换数据源。例如:登录模块就不需要开启,因为他要根据用户名来切换数据源。「仅在多租户模式下此参数才会生效」 |
interceptorExcludePathPatterns | List<String> | - | 数据源过滤器排除拦截路径,使用 spring 的 PathPattern 格式。如果有的 url 需要进行排除,比如 websocket 连接需要自己处理数据源。 |
serviceGroupNumber | String | 租户服务分组编号,用于区分数据源的加载,对应 tenant_service_group 表 group_number 字段,不配置则加载全部 | |
tenantDatabasePrefix | String | tenantdb_ | 租户数据库前缀,会根据租户id自动填充后缀 |
数据源配置说明
只需要在配置文件中加入主库配置信息即可,配置前缀 spring.datasource.dynamic.datasource.master
driver-class-name
数据库连接驱动
MySQL
com.mysql.cj.jdbc.Driver
SQL Server
com.microsoft.sqlserver.jdbc.SQLServerDriver
Oracle
oracle.jdbc.driver.OracleDriver
达梦
dm.jdbc.driver.DmDriver
url
数据库连接
MySQL
jdbc:mysql://ip:port/数据库名?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8
SQL Server
jdbc:sqlserver://ip:port;databaseName=数据库名
Oracle
jdbc:oracle:thin:@ip:port:数据库名
达梦
jdbc:dm://ip:port?schema=数据库名
username
用户名
password
密码
Mapper 通用配置
为了支持 JDK 21 需要将 Mapper 解析器配置为自定义解析器,这个配置要配置在 application.yml 中。
mapper:
resolve-class: com.simperfect.commons.mapper.resolve.JakartaEntityResolve # 配置解析器为我们自定义的解析器,使其支持 Jakarta API
数据源自动更新服务
数据源自动更新服务主要作用是自动更新数据源,如果不需要这个服务,则需要手动更新数据源,则需要配置在 application.yml 中。
simperfect:
datasource:
dynamic:
auto-update-enabled: true # 是否启用多数据自动更新服务,一般程序都需要自动更新数据源。例如:租户管理台就不需要开启,因为它需要手动维护数据源
数据源拦截器
数据源拦截器主要作用是自动切换数据源,如果不需要这个拦截器,需要手动切换数据源,则需要配置在 application.yml 中。
重要
如果启用,则会通过 header
中的 Tenant-Sn
自动切换数据源,如果没有 Tenant-Sn
会返回 403
。切记这个过滤器是在业务应用中,并非 gateway
中。
simperfect:
datasource:
dynamic:
datasource-interceptor-enabled: false # 是否启用数据源拦截器,登录模块不需要过滤器,关闭即可
数据源拦截器白名单
在没有需要任何数据源信息、或者需要自己管理数据源的情况下可以添加到白名单中,这样请求过来就不会被拦截器拦截。这个配置适合在有少数接口需要自己处理数据源时是用。
simperfect:
datasource:
dynamic:
interceptor-exclude-path-patterns:
- '/*/public-resources/**'
- '/*/*/public-resources/**'
配置文件样例
数据源配置要配置在配置中心(Nacos)中的 datasource.yaml 中。
多租户数据源配置
simperfect:
datasource:
dynamic:
enabled: true # 是否启用多租户模式 true-启用 false-禁用
spring:
datasource:
dynamic:
druid:
initial-size: 5 # 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
min-idle: 5 # 最小连接池数量
max-active: 20 # 最大连接池数量
validation-query: select 'x' # 用来检测连接是否有效的sql,要求是一个查询语句,MySQL与sqlserver使用select 'x';Oracle使用select 1 from dual。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
test-on-borrow: false # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-while-idle: true # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
pool-prepared-statements: false # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
max-pool-prepared-statement-per-connection-size: -1 # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1/db-name?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8
username: username
password: password
单租户模式配置
单租户只需将enabled
设置为false
,直接连接对应的数据库即可
simperfect:
datasource:
dynamic:
enabled: false # 是否启用多租户模式 true-启用 false-禁用
spring:
datasource:
dynamic:
druid:
initial-size: 5 # 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
min-idle: 5 # 最小连接池数量
max-active: 20 # 最大连接池数量
validation-query: select 'x' # 用来检测连接是否有效的sql,要求是一个查询语句,MySQL与sqlserver使用select 'x';Oracle使用select 1 from dual。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
test-on-borrow: false # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-while-idle: true # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
pool-prepared-statements: false # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
max-pool-prepared-statement-per-connection-size: -1 # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1/db_name?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8
username: username
password: password
项目融合方案
数据源拦截器切换数据源
使用数据源拦截器切换数据源只需引入依赖即可,工具类检测到开启多租户时会自动添加数据源拦截器,会通过 header 中的 tenant-sn 自动切换数据源。
手动切换数据源
提示
可以使用 DataSourceTaskUtils
工具类来切换数据源并执行任务,具体使用方法可以查看方法注释。
有些场景下我们需要手动切换数据源,如多租户下的定时任务、登录模块、管理配置模块等等
/**
* @author 王金城
* @date 2022/9/26
**/
@Component
@EnableScheduling
public class TestDynamicDataSourceScheduled {
@Resource
private IEmployeeService employeeService;
private static final Logger logger = LoggerFactory.getLogger(TestDynamicDataSourceScheduled.class);
@Scheduled(fixedDelay = 1000 * 60)
public void dynamicDataSource() {
Set<String> tenantSNs = DataSourceUtils.getAllTenantSN();
if (CollectionUtils.isEmpty(tenantSNs)) {
return;
}
for (String tenantSN : tenantSNs) {
try {
DynamicDataSourceContextHolder.push(tenantSN);
List<BpEmployee> bpEmployees = employeeService.queryAllEmployee();
logger.info("员工数据 {}", JSON.toJSONString(bpEmployees));
} catch (Exception e) {
logger.error("定时任务异常", e);
} finally {
// 必须在finally中清除数据源
DynamicDataSourceContextHolder.clear();
}
}
}
}
注意
切换数据源后,一定要记得在合适的位置调用 DynamicDataSourceContextHolder.clear()
清空当前线程的数据源信息。
如果是多次切换数据源,需要一层一层清除数据库可以是用 DynamicDataSourceContextHolder.poll()
方法。
- 清除数据源务必写在
finally
中,防止程序异常无法执行 - 在本次逻辑(线程)结束时再清除
- 如果开启事务,那么将无法切换数据源
数据源监听器
警告
多租户模式下,数据初始化一定要使用 Listener
来处理,禁止使用 runner
来处理多租户数据,因为它仅仅是程序启动时运行,无法监听租户变化。
有些场景需要监听数据源的变化,例如在管理本地缓存是,故内置了一个监听器 IDynamicDataSourceListener
接口,实现后注入 bean
即可进行监听数据源变化。
/**
* 数据源变化监听,只有再启用多数据的时候才会加载
*
* @author 王金城
* @date 2024/1/31
**/
@Component
@ConditionalOnProperty(prefix = "simperfect.datasource.dynamic", name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceListener implements IDynamicDataSourceListener {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceListener.class);
@Resource
private IBpConfigurationService bpConfigurationService;
@Override
public void datasourceChanged(DataSourceChangedInfo data) {
// 只检测新增的数据源数据
if (CollectionUtils.isEmpty(data.getAddKeys())) {
logger.info("检查租户配置数据 - 无需检查,因为无新增数据源");
return;
}
for (String tenantSN : data.getAddKeys()) {
String tenantId = DataSourceUtils.getTenantId(tenantSN);
logger.info("检查租户 [{}] 配置数据 - 开始", tenantId);
try {
DynamicDataSourceContextHolder.push(tenantSN);
bpConfigurationService.checkDataHandler();
} catch (Exception e) {
logger.error("检查租户 [{}] 配置数据时发生异常", tenantId, e);
} finally {
DynamicDataSourceContextHolder.poll();
}
logger.info("检查租户 [{}] 配置数据 - 完成", tenantId);
}
}
}
获取当前线程中的 tenantId
String peek = DynamicDataSourceContextHolder.peek();
String tenantId = DataSourceUtils.getTenantId(peek);
// 或者直接使用
String tenantId = DataSourceUtils.getThreadLocalTenantId();
关于spring事务的注意事项
- 可以继续使用
@Transactional
注解来使用spring事务 - 在spring事务中是不能进行数据源切换的,不能切换数据源、不能切换数据源、不能切换数据源。重要的事情说三遍