多租戶多資料來源切換
在很多系統中,都存在著租戶的概念。更具需求的不同,系統可以分為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便是租戶的身份特徵編號。這些程式碼就不再描述