多租戶多數據源切換

  • 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便是租戶的身份特徵編號。這些程式碼就不再描述!