Appearance
租户体系
租户体系是saas系统的核心。
实现方式
- 天源云支持租户模式、非租户模式,其中租户模式有3种常见的数据存储方式
- 实现不同租户模式的方式
- 通过注解 + 拦截器的方式实现租户数据的隔离
- 程序员sql编码完全无需拼接租户相关的sql(一套业务代码,可以通用所有模式)
- 通过启动配置决定启用何种模式的插件,程序就能按照何种模式来隔离数据。
独立数据源模式
TIP
一个租户独享一个独立的数据库,可以是物理数据库或云数据库,隔离级别最高、最安全、同时成本也更高。
- 优点
可独立部署数据库,数据隔离性好、扩展性高、故障影响小
- 缺点
相对复杂、开发需要注意切换数据源时的事务问题、需要较多的数据库
运行过程
过程
- 新增租户
- 初始化租户数据库表结构和初始数据
- 创建租户数据库
- 连接数据源
- 更新状态
- 自动授权
- 初始化其他服务的数据源
- 查询待初始化的服务
- 手动初始化
- 修改租户状态
新增租户:向def_tenant表插入一条数据,状态为待初始化结构。
java
@Override
protected DefTenant saveBefore(DefTenantSaveVO defTenantSaveVO) {
// 数据初始化
DefTenant tenant = BeanPlusUtil.toBean(defTenantSaveVO, DefTenant.class);
tenant.setStatus(DefTenantStatusEnum.WAIT_INIT_SCHEMA.getCode());
tenant.setRegisterType(DefTenantRegisterTypeEnum.CREATE);
tenant.setReadonly(false);
if (StrUtil.isEmpty(tenant.getCreatedName())) {
DefUser result = defUserService.getByIdCache(ContextUtil.getUserId());
if (result != null) {
tenant.setCreatedName(result.getNickName());
}
}
return tenant;
}
@Override
protected void saveAfter(DefTenantSaveVO defTenantSaveVO, DefTenant defTenant) {
// 保存租户logo
appendixService.save(defTenant.getId(), defTenantSaveVO.getLogos());
}
@Override
protected DefTenant saveBefore(DefTenantSaveVO defTenantSaveVO) {
// 数据初始化
DefTenant tenant = BeanPlusUtil.toBean(defTenantSaveVO, DefTenant.class);
tenant.setStatus(DefTenantStatusEnum.WAIT_INIT_SCHEMA.getCode());
tenant.setRegisterType(DefTenantRegisterTypeEnum.CREATE);
tenant.setReadonly(false);
if (StrUtil.isEmpty(tenant.getCreatedName())) {
DefUser result = defUserService.getByIdCache(ContextUtil.getUserId());
if (result != null) {
tenant.setCreatedName(result.getNickName());
}
}
return tenant;
}
@Override
protected void saveAfter(DefTenantSaveVO defTenantSaveVO, DefTenant defTenant) {
// 保存租户logo
appendixService.save(defTenant.getId(), defTenantSaveVO.getLogos());
}
初始化租户数据库表结构和初始数据
主要步骤:
- 创建租户数据库
仅选择了系统内置数据源时,系统会自动创建租户数据库,自定义数据源需要开发者提前创建好数据库。 默认的租户数据库前缀为 acuity_base,完整的租户数据库名称为 acuity_base_
- 连接数据源
调用dynamic-datasource-spring-boot-starter组件提供的接口,动态连接新建的租户数据源。 acuity-system-server连接上新数据源后,在新的租户数据库中,执行SQL脚本,创建表结构和插入数据。
- 更新状态
修改租户状态为待初始化数据源
- 自动授权
自动给该租户授权基础平台
java
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/defTenant")
@Api(value = "DefTenant", tags = "企业")
public class DefTenantController extends SuperCacheController<DefTenantService, Long, DefTenant, DefTenantSaveVO, DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> {
/**
* 初始化数据
*/
@ApiOperation(value = "初始化数据", notes = "初始化数据")
@PostMapping("/initData")
@WebLog("连接数据源")
public R<Boolean> initData(@Validated @RequestBody DefTenantInitVO tenantConnect) {
return success(superService.initData(tenantConnect));
}
}
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/defTenant")
@Api(value = "DefTenant", tags = "企业")
public class DefTenantController extends SuperCacheController<DefTenantService, Long, DefTenant, DefTenantSaveVO, DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> {
/**
* 初始化数据
*/
@ApiOperation(value = "初始化数据", notes = "初始化数据")
@PostMapping("/initData")
@WebLog("连接数据源")
public R<Boolean> initData(@Validated @RequestBody DefTenantInitVO tenantConnect) {
return success(superService.initData(tenantConnect));
}
}
java
@Slf4j
@Service
@DS(DsConstant.DEFAULTS)
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DefTenantServiceImpl extends SuperCacheServiceImpl<DefTenantManager, Long, DefTenant, DefTenantSaveVO, DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> implements DefTenantService {
// 构造器注入
private final InitSystemContext initSystemContext;
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean initData(DefTenantInitVO tenantConnect) {
// 初始数据 && 更新状态
return initSystemContext.initData(tenantConnect) && updateTenantStatus(tenantConnect);
}
private Boolean updateTenantStatus(DefTenantInitVO initVO) {
Boolean flag = superManager.update(Wraps.<DefTenant>lbU().set(DefTenant::getStatus, DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode())
.set(DefTenant::getConnectType, initVO.getConnectType())
.eq(DefTenant::getId, initVO.getId()));
superManager.delCache(initVO.getId());
// 自动授权基础平台
defTenantApplicationRelManager.grantGeneralApplication(initVO.getId());
return flag;
}
}
@Slf4j
@Service
@DS(DsConstant.DEFAULTS)
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DefTenantServiceImpl extends SuperCacheServiceImpl<DefTenantManager, Long, DefTenant, DefTenantSaveVO, DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> implements DefTenantService {
// 构造器注入
private final InitSystemContext initSystemContext;
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean initData(DefTenantInitVO tenantConnect) {
// 初始数据 && 更新状态
return initSystemContext.initData(tenantConnect) && updateTenantStatus(tenantConnect);
}
private Boolean updateTenantStatus(DefTenantInitVO initVO) {
Boolean flag = superManager.update(Wraps.<DefTenant>lbU().set(DefTenant::getStatus, DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode())
.set(DefTenant::getConnectType, initVO.getConnectType())
.eq(DefTenant::getId, initVO.getId()));
superManager.delCache(initVO.getId());
// 自动授权基础平台
defTenantApplicationRelManager.grantGeneralApplication(initVO.getId());
return flag;
}
}
java
@Service("DATASOURCE_COLUMN")
@Slf4j
@RequiredArgsConstructor
public class DatasourceColumnInitSystemStrategy implements InitSystemStrategy {
private final DefDatasourceConfigManager defDatasourceConfigManager;
private final DefTenantDatasourceConfigRelManager defTenantDatasourceConfigRelManager;
private final DataSourceService dataSourceService;
/**
*
* @param tenantInitVO 链接信息
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean initData(DefTenantInitVO tenantInitVO) {
// 自定义数据源
if (TenantConnectTypeEnum.CUSTOM.eq(tenantInitVO.getConnectType())) {
ArgumentAssert.notNull(tenantInitVO.getBaseDatasourceId(), "请配置基础库数据库链接信息");
List<Long> ids = Collections.singletonList(tenantInitVO.getBaseDatasourceId());
if (tenantInitVO.getExtendDatasourceId() != null) {
ids.add(tenantInitVO.getExtendDatasourceId());
}
List<DefDatasourceConfig> dcList = defDatasourceConfigManager.listByIds(ids);
ArgumentAssert.notEmpty(dcList, "请配置正确的基础库和扩展库数据库链接信息");
// 存储租户自定义的数据源信息
defTenantDatasourceConfigRelManager.remove(Wraps.<DefTenantDatasourceConfigRel>lbQ().eq(DefTenantDatasourceConfigRel::getTenantId, tenantInitVO.getId()));
List<DefTenantDatasourceConfigRel> list = new ArrayList<>();
list.add(DefTenantDatasourceConfigRel.builder().dbPrefix(TENANT_BASE_POOL_NAME_HEADER).tenantId(tenantInitVO.getId())
.datasourceConfigId(tenantInitVO.getBaseDatasourceId()).build());
if (tenantInitVO.getExtendDatasourceId() != null) {
list.add(DefTenantDatasourceConfigRel.builder().dbPrefix(TENANT_EXTEND_POOL_NAME_HEADER).tenantId(tenantInitVO.getId())
.datasourceConfigId(tenantInitVO.getExtendDatasourceId()).build());
}
defTenantDatasourceConfigRelManager.saveBatch(list);
// 动态添加租户服务的数据源链接 & 创建表 & 初始数据
dataSourceService.addCustomDsAndData(tenantInitVO.getId());
}
// 系统内置数据源
else if (TenantConnectTypeEnum.SYSTEM.eq(tenantInitVO.getConnectType())) {
// 在程序启动时连接的默认库中,创建租户的database
dataSourceService.createDatabase(tenantInitVO.getId());
// 2. 动态添加租户服务的数据源链接 & 创建表 & 初始数据
dataSourceService.addSystemDsAndData(tenantInitVO.getId());
}
return true;
}
}
@Service("DATASOURCE_COLUMN")
@Slf4j
@RequiredArgsConstructor
public class DatasourceColumnInitSystemStrategy implements InitSystemStrategy {
private final DefDatasourceConfigManager defDatasourceConfigManager;
private final DefTenantDatasourceConfigRelManager defTenantDatasourceConfigRelManager;
private final DataSourceService dataSourceService;
/**
*
* @param tenantInitVO 链接信息
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean initData(DefTenantInitVO tenantInitVO) {
// 自定义数据源
if (TenantConnectTypeEnum.CUSTOM.eq(tenantInitVO.getConnectType())) {
ArgumentAssert.notNull(tenantInitVO.getBaseDatasourceId(), "请配置基础库数据库链接信息");
List<Long> ids = Collections.singletonList(tenantInitVO.getBaseDatasourceId());
if (tenantInitVO.getExtendDatasourceId() != null) {
ids.add(tenantInitVO.getExtendDatasourceId());
}
List<DefDatasourceConfig> dcList = defDatasourceConfigManager.listByIds(ids);
ArgumentAssert.notEmpty(dcList, "请配置正确的基础库和扩展库数据库链接信息");
// 存储租户自定义的数据源信息
defTenantDatasourceConfigRelManager.remove(Wraps.<DefTenantDatasourceConfigRel>lbQ().eq(DefTenantDatasourceConfigRel::getTenantId, tenantInitVO.getId()));
List<DefTenantDatasourceConfigRel> list = new ArrayList<>();
list.add(DefTenantDatasourceConfigRel.builder().dbPrefix(TENANT_BASE_POOL_NAME_HEADER).tenantId(tenantInitVO.getId())
.datasourceConfigId(tenantInitVO.getBaseDatasourceId()).build());
if (tenantInitVO.getExtendDatasourceId() != null) {
list.add(DefTenantDatasourceConfigRel.builder().dbPrefix(TENANT_EXTEND_POOL_NAME_HEADER).tenantId(tenantInitVO.getId())
.datasourceConfigId(tenantInitVO.getExtendDatasourceId()).build());
}
defTenantDatasourceConfigRelManager.saveBatch(list);
// 动态添加租户服务的数据源链接 & 创建表 & 初始数据
dataSourceService.addCustomDsAndData(tenantInitVO.getId());
}
// 系统内置数据源
else if (TenantConnectTypeEnum.SYSTEM.eq(tenantInitVO.getConnectType())) {
// 在程序启动时连接的默认库中,创建租户的database
dataSourceService.createDatabase(tenantInitVO.getId());
// 2. 动态添加租户服务的数据源链接 & 创建表 & 初始数据
dataSourceService.addSystemDsAndData(tenantInitVO.getId());
}
return true;
}
}
java
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
private boolean addSystem(Long tenantId, boolean isInitSchema, boolean isNotErrorRetry) {
DataSourceProperty defDataSourceProperty = properties.getDatasource().get(ContextConstants.DEF_TENANT_ID_STR);
ArgumentAssert.notNull(defDataSourceProperty, "请先配置默认[{}]数据源", ContextConstants.DEF_TENANT_ID_STR);
// 读取acuity.database.initDatabasePrefix 配置的租户前缀,动态初始化数据库
databaseProperties.getInitDatabasePrefix().forEach(database -> {
// 在程序启动时配置的默认库 数据源配置的基础上,修改租户库自己的特殊配置
DataSourceProperty newDataSourceProperty = BeanUtil.toBean(defDataSourceProperty, DataSourceProperty.class);
newDataSourceProperty.setPoolName(DsThreadProcessor.getPoolName(database, String.valueOf(tenantId)));
if (DbType.ORACLE.getDb().equals(getDbType().getDb())) {
newDataSourceProperty.setUsername(newDataSourceProperty.getPoolName());
newDataSourceProperty.setPassword(newDataSourceProperty.getPoolName());
} else {
String oldDatabase = DbPlusUtil.getDataBaseNameByUrl(defDataSourceProperty.getUrl());
String newDatabase = StrUtil.join(StrUtil.UNDERLINE, database, tenantId);
newDataSourceProperty.setUrl(StrUtil.replace(defDataSourceProperty.getUrl(), oldDatabase, newDatabase));
}
if (isInitSchema) {
DatasourceInitProperties init = newDataSourceProperty.getInit();
if (init == null) {
init = new DatasourceInitProperties();
}
// 待创建的表结构
init.setSchema(StrUtil.format(SCHEMA_PATH, getDbType().getDb(), database));
// 待初始化的数据
init.setData(StrUtil.format(DATA_PATH, getDbType().getDb(), database));
newDataSourceProperty.setInit(init);
}
newDataSourceProperty.setSeata(databaseProperties.getIsSeata());
newDataSourceProperty.setDruid(properties.getDruid());
if (isNotErrorRetry) {
// 链接错误重试次数
newDataSourceProperty.getDruid().setConnectionErrorRetryAttempts(0);
// 获取失败后中断
newDataSourceProperty.getDruid().setBreakAfterAcquireFailure(true);
}
// 动态新增数据源
putDs(newDataSourceProperty);
});
return true;
}
private Set<String> putDs(DataSourceProperty dsp) {
try {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) this.dataSource;
DataSource newDataSource = druidDataSourceCreator.createDataSource(dsp);
ds.addDataSource(dsp.getPoolName(), newDataSource);
return ds.getDataSources().keySet();
} catch (ErrorCreateDataSourceException e) {
log.error("数据源初始化期间出现异常", e);
throw new BizException("数据源初始化期间出现异常", e);
}
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
private boolean addSystem(Long tenantId, boolean isInitSchema, boolean isNotErrorRetry) {
DataSourceProperty defDataSourceProperty = properties.getDatasource().get(ContextConstants.DEF_TENANT_ID_STR);
ArgumentAssert.notNull(defDataSourceProperty, "请先配置默认[{}]数据源", ContextConstants.DEF_TENANT_ID_STR);
// 读取acuity.database.initDatabasePrefix 配置的租户前缀,动态初始化数据库
databaseProperties.getInitDatabasePrefix().forEach(database -> {
// 在程序启动时配置的默认库 数据源配置的基础上,修改租户库自己的特殊配置
DataSourceProperty newDataSourceProperty = BeanUtil.toBean(defDataSourceProperty, DataSourceProperty.class);
newDataSourceProperty.setPoolName(DsThreadProcessor.getPoolName(database, String.valueOf(tenantId)));
if (DbType.ORACLE.getDb().equals(getDbType().getDb())) {
newDataSourceProperty.setUsername(newDataSourceProperty.getPoolName());
newDataSourceProperty.setPassword(newDataSourceProperty.getPoolName());
} else {
String oldDatabase = DbPlusUtil.getDataBaseNameByUrl(defDataSourceProperty.getUrl());
String newDatabase = StrUtil.join(StrUtil.UNDERLINE, database, tenantId);
newDataSourceProperty.setUrl(StrUtil.replace(defDataSourceProperty.getUrl(), oldDatabase, newDatabase));
}
if (isInitSchema) {
DatasourceInitProperties init = newDataSourceProperty.getInit();
if (init == null) {
init = new DatasourceInitProperties();
}
// 待创建的表结构
init.setSchema(StrUtil.format(SCHEMA_PATH, getDbType().getDb(), database));
// 待初始化的数据
init.setData(StrUtil.format(DATA_PATH, getDbType().getDb(), database));
newDataSourceProperty.setInit(init);
}
newDataSourceProperty.setSeata(databaseProperties.getIsSeata());
newDataSourceProperty.setDruid(properties.getDruid());
if (isNotErrorRetry) {
// 链接错误重试次数
newDataSourceProperty.getDruid().setConnectionErrorRetryAttempts(0);
// 获取失败后中断
newDataSourceProperty.getDruid().setBreakAfterAcquireFailure(true);
}
// 动态新增数据源
putDs(newDataSourceProperty);
});
return true;
}
private Set<String> putDs(DataSourceProperty dsp) {
try {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) this.dataSource;
DataSource newDataSource = druidDataSourceCreator.createDataSource(dsp);
ds.addDataSource(dsp.getPoolName(), newDataSource);
return ds.getDataSources().keySet();
} catch (ErrorCreateDataSourceException e) {
log.error("数据源初始化期间出现异常", e);
throw new BizException("数据源初始化期间出现异常", e);
}
}
}
初始化其他服务的数据源
查询待初始化的服务
acuity-cloud 项目,此代码位于acuity-gateway-server;acuity-boot项目,此代码位于acuity-boot-server
java@Controller public class GeneratorController { @Autowired private DiscoveryClient discoveryClient; @Autowired private GatewayProperties gatewayProperties; @ResponseBody @ApiOperation(value = "查询在线服务的前缀") @GetMapping("/gateway/findOnlineServicePrefix") public R<Map<String, String>> findOnlineServicePrefix() { List<String> services = discoveryClient.getServices(); // 遍历出acuity-gateway-server.yml中正确配置了,且正常注册到nacos中的所有服务 Map<String, String> map = MapUtil.newHashMap(); services.forEach(service -> gatewayProperties.getRoutes().forEach(route -> { if (StrUtil.equalsIgnoreCase(service, route.getUri().getHost())) { if (CollUtil.isEmpty(route.getPredicates())) { return; } PredicateDefinition predicateDefinition = route.getPredicates().get(0); predicateDefinition.getArgs().forEach((k, v) -> { map.put(service, StrUtil.subBetween(v, "/", "/**")); }); } }) ); return R.success(map); } }
@Controller public class GeneratorController { @Autowired private DiscoveryClient discoveryClient; @Autowired private GatewayProperties gatewayProperties; @ResponseBody @ApiOperation(value = "查询在线服务的前缀") @GetMapping("/gateway/findOnlineServicePrefix") public R<Map<String, String>> findOnlineServicePrefix() { List<String> services = discoveryClient.getServices(); // 遍历出acuity-gateway-server.yml中正确配置了,且正常注册到nacos中的所有服务 Map<String, String> map = MapUtil.newHashMap(); services.forEach(service -> gatewayProperties.getRoutes().forEach(route -> { if (StrUtil.equalsIgnoreCase(service, route.getUri().getHost())) { if (CollUtil.isEmpty(route.getPredicates())) { return; } PredicateDefinition predicateDefinition = route.getPredicates().get(0); predicateDefinition.getArgs().forEach((k, v) -> { map.put(service, StrUtil.subBetween(v, "/", "/**")); }); } }) ); return R.success(map); } }
手动初始化
调用各个服务的初始化数据源接口,让其他服务加载数据源。 此代码位于acuity-tenant-datasource-init模块,后台每个业务服务都依赖他。
java@Slf4j @RestController @RequestMapping("/ds") @Api(value = "TenantDs", tags = "数据源") @RequiredArgsConstructor public class TenantDsController { private final DataSourceService dataSourceService; @ApiOperation(value = "初始化数据源", notes = "初始化数据源") @PostMapping(value = "/initDataSource") public R<Boolean> initDataSource(@RequestParam Long tenantId) { return R.success(dataSourceService.initDataSource(tenantId)); } @GetMapping("/check") @ApiOperation("检测是否存在指定数据源") public R<Boolean> check(@RequestParam(value = "tenantId") Long tenantId) { return R.success(dataSourceService.check(tenantId)); } }
@Slf4j @RestController @RequestMapping("/ds") @Api(value = "TenantDs", tags = "数据源") @RequiredArgsConstructor public class TenantDsController { private final DataSourceService dataSourceService; @ApiOperation(value = "初始化数据源", notes = "初始化数据源") @PostMapping(value = "/initDataSource") public R<Boolean> initDataSource(@RequestParam Long tenantId) { return R.success(dataSourceService.initDataSource(tenantId)); } @GetMapping("/check") @ApiOperation("检测是否存在指定数据源") public R<Boolean> check(@RequestParam(value = "tenantId") Long tenantId) { return R.success(dataSourceService.check(tenantId)); } }
修改租户状态
此代码位于acuity-system-server。
javapublic class DefTenantController extends SuperCacheController<DefTenantService, Long, DefTenant, DefTenantSaveVO,DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> { @ApiOperation(value = "修改租户审核状态", notes = "修改租户审核状态") @PostMapping("/updateStatus") @WebLog("修改租户审核状态") public R<Boolean> updateStatus(@NotNull(message = "请修改正确的企业") @RequestParam Long id, @RequestParam @NotNull(message = "请传递状态值") String status, @RequestParam(required = false) String reviewComments) { return success(superService.updateStatus(id, status, reviewComments)); } }
public class DefTenantController extends SuperCacheController<DefTenantService, Long, DefTenant, DefTenantSaveVO,DefTenantUpdateVO, DefTenantPageQuery, DefTenantResultVO> { @ApiOperation(value = "修改租户审核状态", notes = "修改租户审核状态") @PostMapping("/updateStatus") @WebLog("修改租户审核状态") public R<Boolean> updateStatus(@NotNull(message = "请修改正确的企业") @RequestParam Long id, @RequestParam @NotNull(message = "请传递状态值") String status, @RequestParam(required = false) String reviewComments) { return success(superService.updateStatus(id, status, reviewComments)); } }
启动过程
除了系统正常运行时,实时新增租户需要动态创建新租户的数据源,当服务重启时,程序也需要将已经成功创建的租户数据源加载到服务内存中。但database.yml配置文件中,却只配置了默认库的数据库信息。所以,在系统启动时,需要查询数据库读取所有租户的信息,动态创建租户的数据源。 大致流程为:监听系统启动成功事件->查询租户表->初始化租户的数据源
yaml
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
datasource:
dynamic:
enabled: true
# 从这里开始(dynamic),中间的这段配置用于 acuity.database.multiTenantType == DATASOURCE 时
p6spy: ${acuity.database.p6spy:false}
seata: ${acuity.database.isSeata:false}
primary: "0"
datasource:
"0": # 只配置了默认数据库 acuity_defaults
<<: *db-mysql
druid:
<<: *druid-mysql
initialSize: 10
minIdle: 10
maxActive: 200
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
min-evictable-idle-time-millis: 300000 #配置一个连接在池中最小生存的时间,单位是毫秒
filters: stat,wall
wall:
enabled: true
strictSyntaxCheck: false
comment-allow: true
multiStatementAllow: true
noneBaseStatementAllow: true
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
datasource:
dynamic:
enabled: true
# 从这里开始(dynamic),中间的这段配置用于 acuity.database.multiTenantType == DATASOURCE 时
p6spy: ${acuity.database.p6spy:false}
seata: ${acuity.database.isSeata:false}
primary: "0"
datasource:
"0": # 只配置了默认数据库 acuity_defaults
<<: *db-mysql
druid:
<<: *druid-mysql
initialSize: 10
minIdle: 10
maxActive: 200
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
min-evictable-idle-time-millis: 300000 #配置一个连接在池中最小生存的时间,单位是毫秒
filters: stat,wall
wall:
enabled: true
strictSyntaxCheck: false
comment-allow: true
multiStatementAllow: true
noneBaseStatementAllow: true
监听系统启动成功事件
java
@Configuration
public class DatasourceColumnAutoConfiguration {
/**
* 项目启动时,初始化数据源
*/
@Bean
public InitDatabaseOnStarted getInitDatabaseOnStarted(DatasourceInitDataSourceService initSystemContext) {
return new InitDatabaseOnStarted(initSystemContext);
}
}
@Configuration
public class DatasourceColumnAutoConfiguration {
/**
* 项目启动时,初始化数据源
*/
@Bean
public InitDatabaseOnStarted getInitDatabaseOnStarted(DatasourceInitDataSourceService initSystemContext) {
return new InitDatabaseOnStarted(initSystemContext);
}
}
java
@AllArgsConstructor
public class InitDatabaseOnStarted implements ApplicationListener<ApplicationStartedEvent> {
private final DatasourceInitDataSourceService datasourceInitDataSourceService;
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
datasourceInitDataSourceService.initDataSource();
}
}
@AllArgsConstructor
public class InitDatabaseOnStarted implements ApplicationListener<ApplicationStartedEvent> {
private final DatasourceInitDataSourceService datasourceInitDataSourceService;
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
datasourceInitDataSourceService.initDataSource();
}
}
查询租户表
在acuity_defautls库中查询def_tenant表的数据。 在SpringBoot触发ApplicationStartedEvent事件时,mybatis已经读取了database.yml中配置的默认数据源,并成功加载,所以这个时候已经可以查询默认数据库了。
java
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
// 加载系统内置租户数据源
@Override
public boolean loadSystemDataSource() {
// 查询所有可用的租户
List<String> status = Arrays.asList(DefTenantStatusEnum.NORMAL.getCode(), DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode());
List<Long> list = initDbMapper.selectTenantCodeList(status, TenantConnectTypeEnum.SYSTEM.name());
// 循环初始化系统内置数据源
list.forEach(tenantId -> addSystem(tenantId, false, false));
return true;
}
// 加载自定义租户数据源
@Override
public boolean loadCustomDataSource() {
// 查询所有可用的租户
List<String> status = Arrays.asList(DefTenantStatusEnum.NORMAL.getCode(), DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode());
List<DefDatasourceConfigBO> dcList = initDbMapper.selectDataSourceConfig(status, TenantConnectTypeEnum.CUSTOM.name());
// 循环初始化自定义数据源
return addCustom(dcList, false, false);
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
// 加载系统内置租户数据源
@Override
public boolean loadSystemDataSource() {
// 查询所有可用的租户
List<String> status = Arrays.asList(DefTenantStatusEnum.NORMAL.getCode(), DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode());
List<Long> list = initDbMapper.selectTenantCodeList(status, TenantConnectTypeEnum.SYSTEM.name());
// 循环初始化系统内置数据源
list.forEach(tenantId -> addSystem(tenantId, false, false));
return true;
}
// 加载自定义租户数据源
@Override
public boolean loadCustomDataSource() {
// 查询所有可用的租户
List<String> status = Arrays.asList(DefTenantStatusEnum.NORMAL.getCode(), DefTenantStatusEnum.WAIT_INIT_DATASOURCE.getCode());
List<DefDatasourceConfigBO> dcList = initDbMapper.selectDataSourceConfig(status, TenantConnectTypeEnum.CUSTOM.name());
// 循环初始化自定义数据源
return addCustom(dcList, false, false);
}
}
初始化租户的数据源
执行这个方法时,由于isInitSchema=false,所以并不会重复在租户数据库中执行创建表结构和初始化数据。
java
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
private boolean addSystem(Long tenantId, boolean isInitSchema, boolean isNotErrorRetry) {
DataSourceProperty defDataSourceProperty = properties.getDatasource().get(ContextConstants.DEF_TENANT_ID_STR);
ArgumentAssert.notNull(defDataSourceProperty, "请先配置默认[{}]数据源", ContextConstants.DEF_TENANT_ID_STR);
// 读取acuity.database.initDatabasePrefix 配置的租户前缀,动态初始化数据库
databaseProperties.getInitDatabasePrefix().forEach(database -> {
// 在程序启动时配置的默认库 数据源配置的基础上,修改租户库自己的特殊配置
DataSourceProperty newDataSourceProperty = BeanUtil.toBean(defDataSourceProperty, DataSourceProperty.class);
newDataSourceProperty.setPoolName(DsThreadProcessor.getPoolName(database, String.valueOf(tenantId)));
if (DbType.ORACLE.getDb().equals(getDbType().getDb())) {
newDataSourceProperty.setUsername(newDataSourceProperty.getPoolName());
newDataSourceProperty.setPassword(newDataSourceProperty.getPoolName());
} else {
String oldDatabase = DbPlusUtil.getDataBaseNameByUrl(defDataSourceProperty.getUrl());
String newDatabase = StrUtil.join(StrUtil.UNDERLINE, database, tenantId);
newDataSourceProperty.setUrl(StrUtil.replace(defDataSourceProperty.getUrl(), oldDatabase, newDatabase));
}
if (isInitSchema) {
DatasourceInitProperties init = newDataSourceProperty.getInit();
if (init == null) {
init = new DatasourceInitProperties();
}
// 待创建的表结构
init.setSchema(StrUtil.format(SCHEMA_PATH, getDbType().getDb(), database));
// 待初始化的数据
init.setData(StrUtil.format(DATA_PATH, getDbType().getDb(), database));
newDataSourceProperty.setInit(init);
}
newDataSourceProperty.setSeata(databaseProperties.getIsSeata());
newDataSourceProperty.setDruid(properties.getDruid());
if (isNotErrorRetry) {
// 链接错误重试次数
newDataSourceProperty.getDruid().setConnectionErrorRetryAttempts(0);
// 获取失败后中断
newDataSourceProperty.getDruid().setBreakAfterAcquireFailure(true);
}
// 动态新增数据源
putDs(newDataSourceProperty);
});
return true;
}
private Set<String> putDs(DataSourceProperty dsp) {
try {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) this.dataSource;
DataSource newDataSource = druidDataSourceCreator.createDataSource(dsp);
ds.addDataSource(dsp.getPoolName(), newDataSource);
return ds.getDataSources().keySet();
} catch (ErrorCreateDataSourceException e) {
log.error("数据源初始化期间出现异常", e);
throw new BizException("数据源初始化期间出现异常", e);
}
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class DynamicDataSourceServiceImpl implements DataSourceService {
private boolean addSystem(Long tenantId, boolean isInitSchema, boolean isNotErrorRetry) {
DataSourceProperty defDataSourceProperty = properties.getDatasource().get(ContextConstants.DEF_TENANT_ID_STR);
ArgumentAssert.notNull(defDataSourceProperty, "请先配置默认[{}]数据源", ContextConstants.DEF_TENANT_ID_STR);
// 读取acuity.database.initDatabasePrefix 配置的租户前缀,动态初始化数据库
databaseProperties.getInitDatabasePrefix().forEach(database -> {
// 在程序启动时配置的默认库 数据源配置的基础上,修改租户库自己的特殊配置
DataSourceProperty newDataSourceProperty = BeanUtil.toBean(defDataSourceProperty, DataSourceProperty.class);
newDataSourceProperty.setPoolName(DsThreadProcessor.getPoolName(database, String.valueOf(tenantId)));
if (DbType.ORACLE.getDb().equals(getDbType().getDb())) {
newDataSourceProperty.setUsername(newDataSourceProperty.getPoolName());
newDataSourceProperty.setPassword(newDataSourceProperty.getPoolName());
} else {
String oldDatabase = DbPlusUtil.getDataBaseNameByUrl(defDataSourceProperty.getUrl());
String newDatabase = StrUtil.join(StrUtil.UNDERLINE, database, tenantId);
newDataSourceProperty.setUrl(StrUtil.replace(defDataSourceProperty.getUrl(), oldDatabase, newDatabase));
}
if (isInitSchema) {
DatasourceInitProperties init = newDataSourceProperty.getInit();
if (init == null) {
init = new DatasourceInitProperties();
}
// 待创建的表结构
init.setSchema(StrUtil.format(SCHEMA_PATH, getDbType().getDb(), database));
// 待初始化的数据
init.setData(StrUtil.format(DATA_PATH, getDbType().getDb(), database));
newDataSourceProperty.setInit(init);
}
newDataSourceProperty.setSeata(databaseProperties.getIsSeata());
newDataSourceProperty.setDruid(properties.getDruid());
if (isNotErrorRetry) {
// 链接错误重试次数
newDataSourceProperty.getDruid().setConnectionErrorRetryAttempts(0);
// 获取失败后中断
newDataSourceProperty.getDruid().setBreakAfterAcquireFailure(true);
}
// 动态新增数据源
putDs(newDataSourceProperty);
});
return true;
}
private Set<String> putDs(DataSourceProperty dsp) {
try {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) this.dataSource;
DataSource newDataSource = druidDataSourceCreator.createDataSource(dsp);
ds.addDataSource(dsp.getPoolName(), newDataSource);
return ds.getDataSources().keySet();
} catch (ErrorCreateDataSourceException e) {
log.error("数据源初始化期间出现异常", e);
throw new BizException("数据源初始化期间出现异常", e);
}
}
}
警告
为了数据的安全考虑,请勿在acuity_base.sql中加入 DROP TABLE 语句,以防误执行
独立数据源模式扩展
扩展
- 独立数据源模式因为是每个企业是独立的数据库,因此可以在企业内部启用column模式,实现隔离分公司或部门的功能。
- 先独立数据库隔离租户,在按字段隔离下属单位或门店等:每个租户独立一个 数据库(数据源),执行代码时,动态切换数据源,在动态拼接 子租户id 二次隔离。
DATASOURCE_COLUMN模式和DATASOURCE模式的区别在于前者启动时,需要除了加载多数据源,还需要加载一个COLUMN模式的插件: acuityTenantLineInnerInterceptor。 该插件将会拦截Mapper方法,在Mapper类或方法上标记了@TenantLine 注解后,SQL语句将会自动拼接上租户ID的字段或条件。
java
@Slf4j
@Configuration
@EnableConfigurationProperties({DatabaseProperties.class})
@MapperScan(basePackages = {BUSINESS_PACKAGE, UTIL_PACKAGE}, annotationClass = Repository.class)
public class MybatisAutoConfiguration extends BaseMybatisConfiguration {
public MybatisAutoConfiguration(DatabaseProperties databaseProperties) {
super(databaseProperties);
}
@Override
protected List<InnerInterceptor> getPaginationBeforeInnerInterceptor() {
List<InnerInterceptor> list = new ArrayList<>();
if (MultiTenantType.DATASOURCE_COLUMN.eq(databaseProperties.getMultiTenantType())) {
log.info("检查到配置了:{}模式,已加载 column 部分插件", databaseProperties.getMultiTenantType());
// COLUMN 模式 多租户插件
acuityTenantLineInnerInterceptor tli = new acuityTenantLineInnerInterceptor();
tli.setTenantLineHandler(new TenantLineHandler() {
@Override
public String getTenantIdColumn() {
return databaseProperties.getTenantIdColumn();
}
@Override
public boolean ignoreTable(String tableName) {
boolean ignoreTable = databaseProperties.getIgnoreTable() != null && databaseProperties.getIgnoreTable().contains(tableName);
boolean ignoreTablePrefix = databaseProperties.getIgnoreTablePrefix() != null &&
databaseProperties.getIgnoreTablePrefix().stream().anyMatch(prefix -> tableName.startsWith(prefix));
return ignoreTable || ignoreTablePrefix;
}
@Override
public Expression getTenantId() {
// 当前单位ID
// 注意:若用户A不属于任何单位或用户A仅仅属于一个没有单位的部门,自动拼接租户ID时,ContextUtil.getCurrentCompanyId() = null, 将会导致这里报错。 你可以根据业务,将此方法返回null或者throws一个异常。
return new LongValue(ContextUtil.getCurrentCompanyId());
}
});
list.add(tli);
}
return list;
}
}
@Slf4j
@Configuration
@EnableConfigurationProperties({DatabaseProperties.class})
@MapperScan(basePackages = {BUSINESS_PACKAGE, UTIL_PACKAGE}, annotationClass = Repository.class)
public class MybatisAutoConfiguration extends BaseMybatisConfiguration {
public MybatisAutoConfiguration(DatabaseProperties databaseProperties) {
super(databaseProperties);
}
@Override
protected List<InnerInterceptor> getPaginationBeforeInnerInterceptor() {
List<InnerInterceptor> list = new ArrayList<>();
if (MultiTenantType.DATASOURCE_COLUMN.eq(databaseProperties.getMultiTenantType())) {
log.info("检查到配置了:{}模式,已加载 column 部分插件", databaseProperties.getMultiTenantType());
// COLUMN 模式 多租户插件
acuityTenantLineInnerInterceptor tli = new acuityTenantLineInnerInterceptor();
tli.setTenantLineHandler(new TenantLineHandler() {
@Override
public String getTenantIdColumn() {
return databaseProperties.getTenantIdColumn();
}
@Override
public boolean ignoreTable(String tableName) {
boolean ignoreTable = databaseProperties.getIgnoreTable() != null && databaseProperties.getIgnoreTable().contains(tableName);
boolean ignoreTablePrefix = databaseProperties.getIgnoreTablePrefix() != null &&
databaseProperties.getIgnoreTablePrefix().stream().anyMatch(prefix -> tableName.startsWith(prefix));
return ignoreTable || ignoreTablePrefix;
}
@Override
public Expression getTenantId() {
// 当前单位ID
// 注意:若用户A不属于任何单位或用户A仅仅属于一个没有单位的部门,自动拼接租户ID时,ContextUtil.getCurrentCompanyId() = null, 将会导致这里报错。 你可以根据业务,将此方法返回null或者throws一个异常。
return new LongValue(ContextUtil.getCurrentCompanyId());
}
});
list.add(tli);
}
return list;
}
}
字段模式
TIP
共享数据库,共享数据架构:租户共用一个数据库,在业务表中增加字段来区分
- 优点
简单、不复杂、开发无感知
- 缺点
数据隔离性差、安全性差、数据备份和恢复困难.
运行过程
创建租户的大致流程为:新增租户->初始化租户数据库表结构和初始数据->初始化其他服务的数据源。但对于字段模式,创建流程应该为:新增租户->初始化租户初始数据。接下来我们介绍每个步骤的代码实现
过程
- 新增租户
- 初始化租户初始数据
- 初始化租户数据
- 更新状态
- 自动授权
新增租户:向def_tenant表插入一条数据,状态为待初始化结构。
java
@Override
protected DefTenant saveBefore(DefTenantSaveVO defTenantSaveVO) {
// 数据初始化
DefTenant tenant = BeanPlusUtil.toBean(defTenantSaveVO, DefTenant.class);
tenant.setStatus(DefTenantStatusEnum.WAIT_INIT_SCHEMA.getCode());
tenant.setRegisterType(DefTenantRegisterTypeEnum.CREATE);
tenant.setReadonly(false);
if (StrUtil.isEmpty(tenant.getCreatedName())) {
DefUser result = defUserService.getByIdCache(ContextUtil.getUserId());
if (result != null) {
tenant.setCreatedName(result.getNickName());
}
}
return tenant;
}
@Override
protected void saveAfter(DefTenantSaveVO defTenantSaveVO, DefTenant defTenant) {
// 保存租户logo
appendixService.save(defTenant.getId(), defTenantSaveVO.getLogos());
}
@Override
protected DefTenant saveBefore(DefTenantSaveVO defTenantSaveVO) {
// 数据初始化
DefTenant tenant = BeanPlusUtil.toBean(defTenantSaveVO, DefTenant.class);
tenant.setStatus(DefTenantStatusEnum.WAIT_INIT_SCHEMA.getCode());
tenant.setRegisterType(DefTenantRegisterTypeEnum.CREATE);
tenant.setReadonly(false);
if (StrUtil.isEmpty(tenant.getCreatedName())) {
DefUser result = defUserService.getByIdCache(ContextUtil.getUserId());
if (result != null) {
tenant.setCreatedName(result.getNickName());
}
}
return tenant;
}
@Override
protected void saveAfter(DefTenantSaveVO defTenantSaveVO, DefTenant defTenant) {
// 保存租户logo
appendixService.save(defTenant.getId(), defTenantSaveVO.getLogos());
}
初始化租户初始数据
- 初始化租户数据:创建角色信息等需要初始化的数据。java
@Service("COLUMN") @Slf4j @RequiredArgsConstructor public class ColumnInitSystemStrategy implements InitSystemStrategy { private final RoleMapper roleMapper; @Override @Transactional(rollbackFor = Exception.class) public boolean initData(DefTenantInitVO tenantInitVO) { ContextUtil.setTenantBasePoolName(tenantInitVO.getId()); List<SysRole> roles = new ArrayList<>(); roles.add(SysRole.builder().code(RoleConstant.TENANT_ADMIN).name("租户管理员").readonly(true).remarks("内置管理员").state(true) .type(DataTypeEnum.SYSTEM.getCode()).category(RoleCategoryEnum.FUNCTION.getCode()).build()); // 初始化数据 roleMapper.insertBatchSomeColumn(roles); return true; } }
@Service("COLUMN") @Slf4j @RequiredArgsConstructor public class ColumnInitSystemStrategy implements InitSystemStrategy { private final RoleMapper roleMapper; @Override @Transactional(rollbackFor = Exception.class) public boolean initData(DefTenantInitVO tenantInitVO) { ContextUtil.setTenantBasePoolName(tenantInitVO.getId()); List<SysRole> roles = new ArrayList<>(); roles.add(SysRole.builder().code(RoleConstant.TENANT_ADMIN).name("租户管理员").readonly(true).remarks("内置管理员").state(true) .type(DataTypeEnum.SYSTEM.getCode()).category(RoleCategoryEnum.FUNCTION.getCode()).build()); // 初始化数据 roleMapper.insertBatchSomeColumn(roles); return true; } }
- 更新状态-修改租户状态为待初始化数据源
- 自动授权-自动给该租户授权基础平台
启动过程
COLUMN模式启动过程跟DATASOURCE完全不同,COLUMN模式启动时没有依赖dynamic-datasource-spring-boot-starter组件,仅依靠druid加载数据源。在启动时,会注册一个租户SQL拦截器,实现租户SQL的重写。
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);
}
/**
* COLUMN 模式 SQL动态拼接拦截器
*
* @return 插件
*/
@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) {
if (ContextUtil.isEmptyBasePoolNameHeader()) {
return true;
}
boolean ignoreTable = databaseProperties.getIgnoreTable() != null && databaseProperties.getIgnoreTable().contains(tableName);
boolean ignoreTablePrefix = databaseProperties.getIgnoreTablePrefix() != null &&
databaseProperties.getIgnoreTablePrefix().stream().anyMatch(prefix -> tableName.startsWith(prefix));
return ignoreTable || ignoreTablePrefix;
}
@Override
public ValueListExpression getTenantIdList() {
return null;
}
// 自动拼接的租户值
@Override
public Expression getTenantId() {
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);
}
/**
* COLUMN 模式 SQL动态拼接拦截器
*
* @return 插件
*/
@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) {
if (ContextUtil.isEmptyBasePoolNameHeader()) {
return true;
}
boolean ignoreTable = databaseProperties.getIgnoreTable() != null && databaseProperties.getIgnoreTable().contains(tableName);
boolean ignoreTablePrefix = databaseProperties.getIgnoreTablePrefix() != null &&
databaseProperties.getIgnoreTablePrefix().stream().anyMatch(prefix -> tableName.startsWith(prefix));
return ignoreTable || ignoreTablePrefix;
}
@Override
public ValueListExpression getTenantIdList() {
return null;
}
// 自动拼接的租户值
@Override
public Expression getTenantId() {
return new LongValue(ContextUtil.getBasePoolNameHeader());
}
});
list.add(tli);
return list;
}
}
无租户模式
独立系统