在很多系統中,都存在着租戶的概念。更具需求的不同,系統可以分為3種類型
- 方式一:每個租戶有獨立的服務和獨立的數據庫
- 方式二:每個租戶有共享的服務和獨立的數據庫
- 方式三:每個租戶有共享的服務和共享的數據庫
方式1和方式3和我們日常的應用並無不同。但方式二的實現就需要做些改動了
這里我參考了一個主從分離的例子,根據租戶的身份特征選擇相對應的數據源。同時,還應做到動態的添加租戶和數據源
參考了讀寫分離的配置,總共分為4步
1.繼承AbstractRoutingDataSource
public class DynamicDataSource extends AbstractRoutingDataSource {
}
2.添加數據源
每一個數據源都會有一個標識key,數據源和標識key保存在map,通過標識key找到該數據源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
DataSource master = masterDataSource();
DataSource slave = slaveDataSource();
//設置默認數據源
dynamicDataSource.setDefaultTargetDataSource(master);//默認從庫
//配置多數據源
Map<Object, Object> map = new HashMap<>();
map.put(DataSourceType.Master.getName(), master); //key需要跟ThreadLocal中的值對應
map.put(DataSourceType.Slave.getName(), slave);
dynamicDataSource.setTargetDataSources(map);
3.選擇數據源
重寫AbstractRoutingDataSource的determineCurrentLookupKey,即每次想切換數據的時候修改CurrentLookupKey,這樣就能找到該key對應的數據源。
@Override
protected Object determineCurrentLookupKey() {
logger.info("數據源為{}", JdbcContextHolder.getDataSource());
return JdbcContextHolder.getDataSource();
}
4.切面判斷選擇key
數據源選擇
有了上面的基礎,現在我們要實現根據租戶選擇數據源也是非常簡單
我們想讓以下方法自動選擇數據源
我們制定好了任何需要切換數據源的方法首個參數必須是cusId的規則
(注意:項目可以直接從登錄租戶或者其他方法拿到cusId)
public List<Custom> getList(String cusId) {
return customService.list();
}
依然還是4步
前三步都一樣,最后一步我們需要拿到方法的首位參數,代碼如下
1.定義注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AutoDataSource {
DataSourceType value() default DataSourceType.Master;
}
2.切面邏輯
@Before("aspect()")
private void before(JoinPoint point) {
Object target = point.getTarget();
String method = point.getSignature().getName();
Class<?> classz = target.getClass();
Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
try {
Method m = classz.getMethod(method, parameterTypes);
if (m != null && m.isAnnotationPresent(AutoDataSource.class)) {
// AutoDataSource data = m.getAnnotation(AutoDataSource.class);
Object[] args = point.getArgs();
Object sourceKey = args[0];
JdbcContextHolder.putDataSource(sourceKey + "");
logger.info("{}-當前數據源:{}", method, sourceKey);
}
} catch (Exception e) {
e.printStackTrace();
}
}
JdbcContextHolder內部是個ThreadLocal
public class JdbcContextHolder {
private final static ThreadLocal<String> local = new ThreadLocal<>();
public static void putDataSource(String name) {
local.set(name);
}
public static String getDataSource() {
return local.get();
}
3.添加注解即可
@AutoDataSource
public List<Custom> getList(String cusId) {
return customService.list();
}
動態添加數據源
下面要實現的是在不停機的情況下,動態添加數據源。通過上文我們知道,數據源的添加是通過
dynamicDataSource.setTargetDataSources(map)
這行代碼實現的。實際上也就是把我們的數據源信息保存在了AbstractRoutingDataSource的一個map集合中。但是這個map是私有類型的,而且也沒有提供get方法。我們無法直接獲取到map里的數據
@Nullable
private Map<Object, Object> targetDataSources;
有兩種辦法
- 通過反射拿到AbstractRoutingDataSource的targetDataSources
- 自己維護一個map,相當於加了一層代理
這里選擇第二種方法
還是在DynamicDataSource類中創建一個map,並重寫setTargetDataSources方法
private Map<Object, Object> dynamicTargetDataSources = new HashMap<>();
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
this.dynamicTargetDataSources = targetDataSources;
}
在拿到map數據之后我們再添加一個新增方法
/**
* 新增數據源
* @param key 數據源標識
* @param dataSource 數據源
*/
public void addTargetDataSources(Object key, Object dataSource) {
dynamicTargetDataSources.put(key, dataSource);
super.setTargetDataSources(dynamicTargetDataSources);
}
這里好像就有點問題了,數據是加載到TargetDataSources了,但是項目只會在啟動的時候去解析map中的數據。AbstractRoutingDataSource實現了InitializingBean接口,實現了afterPropertiesSet方法,所以我們需要手動觸發下
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
//解析數據源
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
修改下新增的方法,每次都要調用一次afterPropertiesSet
/**
* 新增數據源
* @param key 數據源標識
* @param dataSource 數據源
*/
public void addTargetDataSources(Object key, Object dataSource) {
dynamicTargetDataSources.put(key, dataSource);
super.setTargetDataSources(dynamicTargetDataSources);
super.afterPropertiesSet();
}
接下來就很簡單了,對外暴露一個接口用於新增數據源,數據源的key便是租戶的身份特征編號。這些代碼就不再描述!
