删除旧租户的数据库
DROP DATABASE ?;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

### 租户的多数据源切换的aop
```java
/**
* 租户的多数据源切换的aop
*/
@Aspect
public class TenantSourceExAop implements Ordered {

private final Logger log = LoggerFactory.getLogger(this.getClass());

/**
* 拦截控制器层
*/
@Pointcut("execution(* *..controller.*.*(..))")
public void cutService() {
}

@Around("cutService()")
public Object around(ProceedingJoinPoint point) throws Throwable {
try {
// 根据系统总开关来进行aop
if (ConstantContextHolder.getTenantOpenFlag()) {

// 当前用户已经登陆并且租户信息不为空
if (LoginContextHolder.me().hasLogin()) {
Dict tenantInfo = LoginContextHolder.me().getSysLoginUser().getTenants();
if (tenantInfo != null) {

// 获取当前用户登录的租户标识,切换数据源
String tenantDbName = tenantInfo.getStr(TenantConstants.TENANT_DB_NAME);
if (StrUtil.isNotBlank(tenantDbName)) {
CurrentDataSourceContext.setDataSourceType(tenantDbName);
log.debug(">>> 多租户AOP--TenantSourceExAop--设置数据源为:" + tenantDbName);
}
}
}
}
return point.proceed();
} finally {
log.debug(">>> 多租户AOP--TenantSourceExAop--清空数据源信息!");
CurrentDataSourceContext.clearDataSourceType();
TenantCodeHolder.remove();
TenantDbNameHolder.remove();
}
}

/**
* aop的顺序要早于多数据源切换的
*/
@Override
public int getOrder() {
return AopSortConstant.TENANT_EXCHANGE_AOP;
}

}

详细参考:

https://www.cnblogs.com/zdd-java/p/zdd_datasource_aop.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public abstract class AbstractRoutingDataSource extends AbstractDataSource {

/**
* 子类实现决定最终数据源
*
* @return 数据源
*/
protected abstract DataSource determineDataSource();

@Override
public Connection getConnection() throws SQLException {
return determineDataSource().getConnection();
}

@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineDataSource().getConnection(username, password);
}

@Override
@SuppressWarnings("unchecked")
public <T> T unwrap(Class<T> iface) throws SQLException {
if (iface.isInstance(this)) {
return (T) this;
}
return determineDataSource().unwrap(iface);
}

@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return (iface.isInstance(this) || determineDataSource().isWrapperFor(iface));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DynamicDataSource implements AbstractDataSource { 

@Override
protected DataSource determineDataSource() {

// 获取当前Context存储的数据源名称
String dataSourceType = CurrentDataSourceContext.getDataSourceType();

// 如果当前Context没有值,就用主数据源
if (StrUtil.isEmpty(dataSourceType)) {
dataSourceType = DatabaseConstant.MASTER_DATASOURCE_NAME;
}

// 从数据源容器中获取对应的数据源
Map<String, DataSource> dataSources = DataSourceContext.getDataSources();
return dataSources.get(dataSourceType);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@Import(DataSourcePropertiesConfig.class)
public class MultiDataSourceConfig {

/**
* 多数据源的连接池
*/
@Bean
public DynamicDataSource dataSource() {
return new DynamicDataSource();
}
}
1
2
3
4
5
6
7
8
9
public class RemoveTenantListener implements ApplicationListener<ServletRequestHandledEvent> {

@Override
public void onApplicationEvent(ServletRequestHandledEvent event) {
TenantCodeHolder.remove();
TenantDbNameHolder.remove();
}

}

动态添加租户

a0d451ee1a49eb60f431c3956f8577c3.svg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
@Transactional(rollbackFor = Exception.class)
public void add(TenantInfoParam param) {

// 创建租户数据库
DruidProperties druidProperties = SpringUtil.getBean(DruidProperties.class);
String databaseName = TENANT_DB_PREFIX + param.getCode();
DatabaseUtil.createDatabase(druidProperties, databaseName);

// 创建租户的数据源记录
DatabaseInfoService databaseInfoService;
try {
databaseInfoService = SpringUtil.getBean(DatabaseInfoService.class);
} catch (Exception e) {
throw new TenantException(TenantExceptionEnum.DBS_MODULAR_NOT_ENABLE_ERROR);
}
DatabaseInfoParam dataBaseInfo = DataBaseInfoFactory.createDataBaseInfo(druidProperties, databaseName);
databaseInfoService.add(dataBaseInfo);

// 初始化租户的数据库
SqlRunUtil.runClassPathSql(INIT_SQL_FILE_NAME, databaseName);

// 插入租户记录
TenantInfo tenantInfo = new TenantInfo();
BeanUtil.copyProperties(param, tenantInfo);
tenantInfo.setDbName(databaseName);
this.save(tenantInfo);

// 切换数据源到新的租户,初始化新租户的用户名和密码
String hashPw = BCrypt.hashpw(param.getAdminPassword(), BCrypt.gensalt());
DataSource dataSource = DataSourceContext.getDataSources().get(databaseName);
Connection connection = null;
try {
connection = dataSource.getConnection();
SqlExecutor.execute(connection, UPDATE_SQL, hashPw);
} catch (SQLException e) {
log.error(">>> 更新多租户的用户密码错误!", e);
throw new TenantException(TenantExceptionEnum.UPDATE_TENANT_PASSWORD_ERROR);
} finally {
DbUtil.close(connection);
}
}

一个事务相关的问题

每次调用的时候会走chooseDataSource再走AOP,经查询发现是事务问题导致,因为它先走了事务切面,然后事务还没结束的时候如果再去切换数据源的话是不成立的。方法解决很简单,设置一下切换数据源的AOP的优先级,确保在事务执行之前就已经切换数据源

有事务时不管注解是什么都会取master,spring事务切面优先执行,创建事务时dataSource已经被绑定了,去执行方法时通过切面虽然切换了当前数据源但是,执行sql时是从事务的threadLocal resources 中取,取的是创建事务时的数据库连接。

声明式事务与上文切换数据源的aop都是基于动态代理,或者说后置处理器实现的,标记一个@Order是不是可以解决这个问题?

1647262928453-07f051a8-5dc9-4557-802d-411414566f85.png

https://blog.csdn.net/qq_31156277/article/details/85227415

有那么多dataSource,但都不是Bean,只有一个是Bean,

和Bean的名字么有关系,使用的时候使用的是接口,调用的时候看的是子类,所以这里有多个子类是Bean的时候就会使用@Primary来决定,都没有的话就会抛出异常。

但是本项目里面因为只有一个DataSource,所以不存在这个问题