Appearance
配置和实现
相关配置
在控制台打印SQL语句
提示
database.yml文件中acuity.database.p6spy = true时,启动程序时dynamic-datasource-spring-boot-starter组件将会使用P6spy数据源代理Druid的数据源,以实现SQL语句的输出功能
yaml
acuity:
database:
# 生产环境请设置p6spy = false
p6spy: true
acuity:
database:
# 生产环境请设置p6spy = false
p6spy: true
java
/**
* 抽象连接池创建器
* <p>
* 这里主要处理一些公共逻辑,如脚本和事件等
*/
@Slf4j
public abstract class AbstractDataSourceCreator implements DataSourceCreator {
private DataSource wrapDataSource(DataSource dataSource, DataSourceProperty dataSourceProperty) {
String name = dataSourceProperty.getPoolName();
DataSource targetDataSource = dataSource;
// acuity.database.p6spy = true 时,P6DataSource 会包装原生DataSource
Boolean enabledP6spy = properties.getP6spy() && dataSourceProperty.getP6spy();
if (enabledP6spy) {
targetDataSource = new P6DataSource(dataSource);
log.debug("dynamic-datasource [{}] wrap p6spy plugin", name);
}
// ...
return new ItemDataSource(name, dataSource, targetDataSource, enabledP6spy, enabledSeata, seataMode);
}
}
/**
* 抽象连接池创建器
* <p>
* 这里主要处理一些公共逻辑,如脚本和事件等
*/
@Slf4j
public abstract class AbstractDataSourceCreator implements DataSourceCreator {
private DataSource wrapDataSource(DataSource dataSource, DataSourceProperty dataSourceProperty) {
String name = dataSourceProperty.getPoolName();
DataSource targetDataSource = dataSource;
// acuity.database.p6spy = true 时,P6DataSource 会包装原生DataSource
Boolean enabledP6spy = properties.getP6spy() && dataSourceProperty.getP6spy();
if (enabledP6spy) {
targetDataSource = new P6DataSource(dataSource);
log.debug("dynamic-datasource [{}] wrap p6spy plugin", name);
}
// ...
return new ItemDataSource(name, dataSource, targetDataSource, enabledP6spy, enabledSeata, seataMode);
}
}
修改DATASOURCE模式的租户数据库前缀
yaml
acuity:
mysql:
database: test_defaults # 启动时连接的默认库
database:
initDatabasePrefix:
- test_base # 启动时连接的租户库前缀 务必与 ContextConstants.TENANT_BASE_POOL_NAME_HEADER 保持一致
acuity:
mysql:
database: test_defaults # 启动时连接的默认库
database:
initDatabasePrefix:
- test_base # 启动时连接的租户库前缀 务必与 ContextConstants.TENANT_BASE_POOL_NAME_HEADER 保持一致
java
public final class ContextConstants {
// 切换数据源时租户前缀 务必与 acuity.database.initDatabasePrefix 保持一致
public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base";
}
public final class ContextConstants {
// 切换数据源时租户前缀 务必与 acuity.database.initDatabasePrefix 保持一致
public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base";
}
text
- 微服务版
acuity-system-server/src/main/resources/schema/{数据库类型}/acuity_base.sql 修改为 test_base.sql
acuity-system-server/src/main/resources/data/{数据库类型}/acuity_base.sql 修改为 test_base.sql
- Boot
acuity-boot-server/src/main/resources/schema/{数据库类型}/acuity_base.sql 修改为 test_base.sql
acuity-boot-server/src/main/resources/data/{数据库类型}/acuity_base.sql 修改为 test_base.sql
- 微服务版
acuity-system-server/src/main/resources/schema/{数据库类型}/acuity_base.sql 修改为 test_base.sql
acuity-system-server/src/main/resources/data/{数据库类型}/acuity_base.sql 修改为 test_base.sql
- Boot
acuity-boot-server/src/main/resources/schema/{数据库类型}/acuity_base.sql 修改为 test_base.sql
acuity-boot-server/src/main/resources/data/{数据库类型}/acuity_base.sql 修改为 test_base.sql
服务运行时,操作多个租户库步骤
提示
下面配置表示该服务在启动时,除了acuity_defautls库,还会同时连3*N个租户库, N表示租户表(def_tenant)有多少租户,3表示下面配置中的test_base、test_extend、test_xxx。
修改 database.yml
yaml
acuity:
database:
initDatabasePrefix:
- test_base # 启动时连接的租户库前缀 务必与TENANT_BASE_POOL_NAME_HEADER保持一致
- test_extend # 启动时连接的租户库前缀2 务必与TENANT_EXTEND_POOL_NAME_HEADER保持一致
- test_xxx # 启动时连接的租户库前缀3 需要在ContextConstants中新增一个常量: TENANT_XXX_POOL_NAME_HEADER
acuity:
database:
initDatabasePrefix:
- test_base # 启动时连接的租户库前缀 务必与TENANT_BASE_POOL_NAME_HEADER保持一致
- test_extend # 启动时连接的租户库前缀2 务必与TENANT_EXTEND_POOL_NAME_HEADER保持一致
- test_xxx # 启动时连接的租户库前缀3 需要在ContextConstants中新增一个常量: TENANT_XXX_POOL_NAME_HEADER
修改 ContextConstants
java
public final class ContextConstants {
// [内置常量] 切换数据源时租户前缀
public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base";
// [内置常量]
public static final String TENANT_EXTEND_POOL_NAME_HEADER = "test_extend";
// [自行新增]
public static final String TENANT_XXX_POOL_NAME_HEADER = "test_xxx";
}
public final class ContextConstants {
// [内置常量] 切换数据源时租户前缀
public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base";
// [内置常量]
public static final String TENANT_EXTEND_POOL_NAME_HEADER = "test_extend";
// [自行新增]
public static final String TENANT_XXX_POOL_NAME_HEADER = "test_xxx";
}
修改 ContextUtil
java
public final class ContextUtil {
public static void setTenantId(Object tenantId) {
set(ContextConstants.TENANT_ID_HEADER, tenantId);
setTenantBasePoolName(tenantId);
setTenantExtendPoolName(tenantId);
// 需要自己新增该方法
setTenantXxxPoolName(tenantId);
}
/**
* 切换xxx库
*
* @param tenantId
*/
public static void setTenantXxxPoolName(Object tenantId) {
set(ContextConstants.TENANT_XXX_POOL_NAME_HEADER, tenantId);
}
}
public final class ContextUtil {
public static void setTenantId(Object tenantId) {
set(ContextConstants.TENANT_ID_HEADER, tenantId);
setTenantBasePoolName(tenantId);
setTenantExtendPoolName(tenantId);
// 需要自己新增该方法
setTenantXxxPoolName(tenantId);
}
/**
* 切换xxx库
*
* @param tenantId
*/
public static void setTenantXxxPoolName(Object tenantId) {
set(ContextConstants.TENANT_XXX_POOL_NAME_HEADER, tenantId);
}
}
修改 DsConstant
java
public interface DsConstant {
/**
* 默认数据源
*/
String DEFAULTS = "0";
/**
* [内置常量] 动态租户数据源
*/
String BASE_TENANT = "#thread." + ContextConstants.TENANT_BASE_POOL_NAME_HEADER;
String EXTEND_TENANT = "#thread." + ContextConstants.TENANT_EXTEND_POOL_NAME_HEADER;
// [自行新增]
String XXX_TENANT = "#thread." + ContextConstants.TENANT_XXX_POOL_NAME_HEADER;
}
public interface DsConstant {
/**
* 默认数据源
*/
String DEFAULTS = "0";
/**
* [内置常量] 动态租户数据源
*/
String BASE_TENANT = "#thread." + ContextConstants.TENANT_BASE_POOL_NAME_HEADER;
String EXTEND_TENANT = "#thread." + ContextConstants.TENANT_EXTEND_POOL_NAME_HEADER;
// [自行新增]
String XXX_TENANT = "#thread." + ContextConstants.TENANT_XXX_POOL_NAME_HEADER;
}
编写ServiceImpl代码
java
@DS(DsConstant.BASE_TENANT)
public class ServiceImpl {
// 方法上没有@DS注解就使用类上的
public void test1() {
// CRUD 操作 test_base 库
}
@DS(DsConstant.DEFAULTS)
public void test2() {
// CRUD 操作 test_defaults 库
}
@DS(DsConstant.EXTEND_TENANT)
public void test3() {
// CRUD 操作 test_extend 库
}
@DS(DsConstant.XXX_TENANT)
public void test4() {
// CRUD 操作 test_xxx 库
}
}
@DS(DsConstant.BASE_TENANT)
public class ServiceImpl {
// 方法上没有@DS注解就使用类上的
public void test1() {
// CRUD 操作 test_base 库
}
@DS(DsConstant.DEFAULTS)
public void test2() {
// CRUD 操作 test_defaults 库
}
@DS(DsConstant.EXTEND_TENANT)
public void test3() {
// CRUD 操作 test_extend 库
}
@DS(DsConstant.XXX_TENANT)
public void test4() {
// CRUD 操作 test_xxx 库
}
}
添加SQL脚本
- 微服务
- 在 acuity-system-server/src/main/resources/schema/{数据库类型}/ 目录添加 test_extend.sql
- 在 acuity-system-server/src/main/resources/data/{数据库类型}/ 目录添加 test_xxx.sql
- boot
在 acuity-boot-server/src/main/resources/schema/{数据库类型}/ 目录添加 test_extend.sql 在 acuity-boot-server/src/main/resources/data/{数据库类型}/ 目录添加 test_xxx.sql
代码层面如何切换数据库
提示
使用dynamic-datasource-spring-boot-starter组件来实现操作多数据源,受到这个组件的限制,在需要代码层面上需要切换数据源时,切换的方法上不能加本地事务注解@Transactional
java
public class OrderService {
@Resource
private UserService userService;
@Resource
private ProductService productService;
// 正例
public void save1() {
// 保存 默认库
userService.save();
ContextUtil.setTenantBasePoolName(1L);
// 保存 租户库: acuity_base_1
productService.save();
// 保存 租户库:acuity_base_2
ContextUtil.setTenantBasePoolName(2L);
productService.save();
int a = 1/0; // 虽然能切库成功,但此方法没有事务,报错后数据不会回滚。
}
// 反例 涉及分布式事务需要切库时,不能在调用方方法上加 本地事务,想要解决事务问题,请使用seata分布式事务
@Transactional(rollbackFor = Exception.class) // 这个注解会导致切库失败
public void save2() {
// 保存 默认库
userService.save();
ContextUtil.setTenantBasePoolName(1L);
// 保存 租户库: acuity_base_1
productService.save();
// 保存 租户库:acuity_base_2
ContextUtil.setTenantBasePoolName(2L);
productService.save();
int a = 1/0;
}
}
public class OrderService {
@Resource
private UserService userService;
@Resource
private ProductService productService;
// 正例
public void save1() {
// 保存 默认库
userService.save();
ContextUtil.setTenantBasePoolName(1L);
// 保存 租户库: acuity_base_1
productService.save();
// 保存 租户库:acuity_base_2
ContextUtil.setTenantBasePoolName(2L);
productService.save();
int a = 1/0; // 虽然能切库成功,但此方法没有事务,报错后数据不会回滚。
}
// 反例 涉及分布式事务需要切库时,不能在调用方方法上加 本地事务,想要解决事务问题,请使用seata分布式事务
@Transactional(rollbackFor = Exception.class) // 这个注解会导致切库失败
public void save2() {
// 保存 默认库
userService.save();
ContextUtil.setTenantBasePoolName(1L);
// 保存 租户库: acuity_base_1
productService.save();
// 保存 租户库:acuity_base_2
ContextUtil.setTenantBasePoolName(2L);
productService.save();
int a = 1/0;
}
}
java
public class ProductService {
// 本地事务 #thread.xxx 表示动态获取 acuity_base的数据源
@DS("#thread.acuity_base")
@Transactional(rollbackFor = Exception.class)
public void save(Product product) {
dao.insert(product);
}
}
public class ProductService {
// 本地事务 #thread.xxx 表示动态获取 acuity_base的数据源
@DS("#thread.acuity_base")
@Transactional(rollbackFor = Exception.class)
public void save(Product product) {
dao.insert(product);
}
}
java
public class UserService {
// 本地事务 0表示在yaml中配置的固定数据源
@DS("0")
@Transactional(rollbackFor = Exception.class)
public void save(User user) {
dao.insert(user);
}
}
public class UserService {
// 本地事务 0表示在yaml中配置的固定数据源
@DS("0")
@Transactional(rollbackFor = Exception.class)
public void save(User user) {
dao.insert(user);
}
}
修改自动拼接的SQL中租户ID的字段名
字段模式适用
yaml
# database.yml
acuity:
database:
tenantIdColumn: tenant_id
# database.yml
acuity:
database:
tenantIdColumn: tenant_id
修改自动拼接的SQL中租户ID的值
字段模式适用
- 方法1:全局配置修改
- 方法2:在代码中临时修改
全局配置修改
java
@Configuration
@Slf4j
@EnableConfigurationProperties({DatabaseProperties.class})
@MapperScan(basePackages = {UTIL_PACKAGE, BUSINESS_PACKAGE}, annotationClass = Repository.class)
public class MybatisAutoConfiguration extends BaseMybatisConfiguration {
public MybatisAutoConfiguration(final DatabaseProperties databaseProperties) {
super(databaseProperties);
}
@Override
protected List<InnerInterceptor> getPaginationBeforeInnerInterceptor() {
List<InnerInterceptor> list = new ArrayList<>();
// COLUMN 模式 多租户插件
TenantLineInnerInterceptor tli = new TenantLineInnerInterceptor();
tli.setTenantLineHandler(new MultiTenantLineHandler() {
// 租户字段的数据库字段名
@Override
public String getTenantIdColumn() {
return databaseProperties.getTenantIdColumn();
}
@Override
public boolean ignoreTable(String tableName) {
return false
}
// 自动拼接的租户值
@Override
public Expression getTenantId() {
// 1. 可以修改这里
return new LongValue(ContextUtil.getBasePoolNameHeader());
}
});
list.add(tli);
return list;
}
}
@Configuration
@Slf4j
@EnableConfigurationProperties({DatabaseProperties.class})
@MapperScan(basePackages = {UTIL_PACKAGE, BUSINESS_PACKAGE}, annotationClass = Repository.class)
public class MybatisAutoConfiguration extends BaseMybatisConfiguration {
public MybatisAutoConfiguration(final DatabaseProperties databaseProperties) {
super(databaseProperties);
}
@Override
protected List<InnerInterceptor> getPaginationBeforeInnerInterceptor() {
List<InnerInterceptor> list = new ArrayList<>();
// COLUMN 模式 多租户插件
TenantLineInnerInterceptor tli = new TenantLineInnerInterceptor();
tli.setTenantLineHandler(new MultiTenantLineHandler() {
// 租户字段的数据库字段名
@Override
public String getTenantIdColumn() {
return databaseProperties.getTenantIdColumn();
}
@Override
public boolean ignoreTable(String tableName) {
return false
}
// 自动拼接的租户值
@Override
public Expression getTenantId() {
// 1. 可以修改这里
return new LongValue(ContextUtil.getBasePoolNameHeader());
}
});
list.add(tli);
return list;
}
}
在代码中临时修改
java
public class UserServiceImpl {
public void save() {
// 2 调用ContextUtil.setBasePoolNameHeader这个方法,就能影响拼接到SQL中的租户ID值
ContextUtil.setBasePoolNameHeader(4L);
dao.insert(user);
}
}
public class UserServiceImpl {
public void save() {
// 2 调用ContextUtil.setBasePoolNameHeader这个方法,就能影响拼接到SQL中的租户ID值
ContextUtil.setBasePoolNameHeader(4L);
dao.insert(user);
}
}
代码层面如何切换数据库
切库时使用mybatis-plus组件来实现拦截SQL语句,动态添加租户条件的目的
java
public class UserServiceImpl {
@Transactional(rollbackFor = Exception.class) // 事务不影响 COLUMN模式 切租户
public void save() {
// insert into user (id, tenant_id, name) values (1, 4, 'xxx');
ContextUtil.setBasePoolNameHeader(4L);
dao.insert(user);
// insert into user (id, tenant_id, name) values (2, 5, 'xxx');
ContextUtil.setBasePoolNameHeader(5L);
dao.insert(user);
}
}
public class UserServiceImpl {
@Transactional(rollbackFor = Exception.class) // 事务不影响 COLUMN模式 切租户
public void save() {
// insert into user (id, tenant_id, name) values (1, 4, 'xxx');
ContextUtil.setBasePoolNameHeader(4L);
dao.insert(user);
// insert into user (id, tenant_id, name) values (2, 5, 'xxx');
ContextUtil.setBasePoolNameHeader(5L);
dao.insert(user);
}
}
java
public interface UserMapper {
public int insert(User user);
}
public interface UserMapper {
public int insert(User user);
}