1. 程式人生 > >多租戶多資料來源切換

多租戶多資料來源切換

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