多租戶多數據源切換
- 2019 年 10 月 3 日
- 筆記
在很多系統中,都存在著租戶的概念。更具需求的不同,系統可以分為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便是租戶的身份特徵編號。這些程式碼就不再描述!