为了做隔离性比较好的multi-tenant app,决定做schema based multi-tenant,这就需要
- Runtime切换DataSource
- 为了使添加新tenant不用重启,最好还能实现Runtime添加DataSource
所以分2步实现这两个功能。
代码:https://github.com/fanjingdan012/ssm
目前有3个branch
- master是基础版Spring+Mybatis+Mariadb,能Read一个DataSource
- multi-data-source是实现Runtime切换DataSource的
- multi-tenant是实现现Runtime添加DataSource的
预先定义DataSource,Runtime切换
效果
- 数据库准备,这里用了mariadb,用了两个schema, test和test2,里面是同样的一张member表,插入一点不同的数据
- 用header控制Tenant-ID,从而访问不同的DataSource
代码
写一个MultitenantDataSource.java,MultitenantDataSource extends AbstractRoutingDataSource
看一下AbstractRoutingDataSource
的源代码
1 | protected DataSource determineTargetDataSource() { |
里面有一个resolvedDataSources
的Map,存储了多个DataSource,会调用determineCurrentLookupKey()
来Runtime决定使用哪个DataSource,如果没有指定的那个Key,那么就会使用DefaultDataSource
所以使用它就是需要
- Override
determineCurrentLookupKey()
方法,定义tenantId作为key1
2
3
4
5
6public class MultitenantDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
} - 调用
setTargetDataSources
方法去把Map填进去 - 调用
setDefaultTargetDataSource
方法把DefaultDataSource设置好,最好是将DefaultDataSource设置成tenant管理数据库,保存tenant相关信息,但是作为一个demo,本项目就比较简单,直接把它设为一个tenant数据库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
public class MultitenantConfiguration {
public MultitenantDataSource multitenantDataSource() {
Map<Object,Object> resolvedDataSources = new HashMap<>();
//db1
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create()
.url("jdbc:mysql://localhost/test")
.username("root")
.password("******");
resolvedDataSources.put("tenant1",dataSourceBuilder.build());
resolvedDataSources.put("Default",dataSourceBuilder.build());
//db2
DataSourceBuilder dataSourceBuilder2 = DataSourceBuilder.create()
.url("jdbc:mysql://localhost/test2")
.username("root")
.password("******");
resolvedDataSources.put("tenant2",dataSourceBuilder2.build());
MultitenantDataSource dataSource = new MultitenantDataSource();
dataSource.setDefaultTargetDataSource(resolvedDataSources.get("Default"));
dataSource.setTargetDataSources(resolvedDataSources);
dataSource.afterPropertiesSet();
return dataSource;
} - 在Controller里添加从header读取tenantId的逻辑
XXController.java
1
2
3
4public Member member( String name, String tenantName){
TenantContext.setCurrentTenant(tenantName);
...
}
Runtime 添加DataSource
效果
- 注册新的tenant,直接把jdbc url,username, password通过request parameter传入,返回success
- 通过header控制tenantId,访问新的tenant(DataSource)的数据
代码
- 写一个
MultitenantDataSourceRegister.java
(implements ImportBeanDefinitionRegistrar
), 就要实现registerBeanDefinitions
方法1
2
3
4
5
6
7
8
9
10
11public void registerBeanDefinitions(AnnotationMetadata annotaion, BeanDefinitionRegistry registry) {
System.out.println("registerBeanDefinitions");
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(MultitenantDataSource.class);
beanDefinition.setSynthetic(true);
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
mpv.addPropertyValue("defaultTargetDataSource", getDefaultDataSources().get("Default"));
mpv.addPropertyValue("targetDataSources",getDefaultDataSources());
registry.registerBeanDefinition("dataSource", beanDefinition);
} - 在
MultitenantConfiguration
上添加@Import(MultitenantDataSourceRegister.class)
1
2
3
4
5
6
7
8
public class MultitenantConfiguration {
private MultitenantDataSource multitenantDataSource;
} - 在
MultitenantDataSource
里维护一个Map用来管理DataSources1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class MultitenantDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
private ConcurrentHashMap<String, DataSource> backupTargetDataSources = new ConcurrentHashMap<>();
public void addDataSourceToTargetDataSource(String key ,DataSource ds){
this.backupTargetDataSources.put(key, ds);
this.setTargetDataSource(this.backupTargetDataSources);
}
public void setTargetDataSource(Map targetDataSource){
super.setTargetDataSources(targetDataSource);
this.afterPropertiesSet();
}
} - 在
XXController
里添加注册DataSource的API1
2
3
4
5
6
7
8
9
10
11
public String tenantRegister( String username, String password, String url, String tenantName){
MultitenantDataSource multitenantDataSource = ctx.getBean(MultitenantDataSource.class);
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create()
.url(url)
.username(username)
.password(password);
multitenantDataSource.addDataSourceToTargetDataSource(tenantName, dataSourceBuilder.build());
return "success";
}