關於(歷史)系統多租戶設計的後續

相關文章:

因爲某些原因,臨近上線前我們調整了方案,即使用多數據源的方案去對系統進行多租戶改造,這也是《基於 MyBatis 實現多租戶數據隔離的實踐》中與各位夥伴討論的相對好的方案。這樣改造過程平滑,兩種方案(數據合併方案和多數據源方案)的風險、操作難度不在一個數量級。

雖然多數據源方案相對簡單很多,但還是要注意一些問題。這裏將一些問題記錄一下。

(歷史)系統多數據源配置

AbstractRoutingDataSource

其實在 Spring/Spring Boot 中多數據源並不是什麼麻煩的事情,一般項目都是使用的 AbstractRoutingDataSource 進行多數據源控制。但是歷史系統都有一個問題就是“註釋很少、會有一定程度的封裝”,造成很多功能在改造的時候會有難度。

所以這部分在改造的時候需要把握 AbstractRoutingDataSource 的核心方法。其實這個類就是一個模版類。關鍵要注意這個方法:

protected abstract Object determineCurrentLookupKey();

很明顯需要子類去實現。方法名中有“Key”,在 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineTargetDataSource 方法中:

protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
      //設置默認數據源,根據 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#setDefaultTargetDataSource 設置
			dataSource = this.resolvedDefaultDataSource;
		}
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}

可以看到,resolvedDataSources 就是根據 determineCurrentLookupKey 方法返回的 Key 去獲取數據源。那麼 resolvedDataSources 是從哪來的呢:

@Override
	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);
		}
	}

可以看到 resolvedDataSources 的數據來自於 targetDataSources,而 AbstractRoutingDataSource 也給我們提供了 setTargetDataSources 方法:

	public void setTargetDataSources(Map<Object, Object> targetDataSources) {
		this.targetDataSources = targetDataSources;
	}

所以我們可以在子類中將數據源初始化好之後設置到 targetDataSources 即可。

關於多數據源的概念和 @MapperScan

多數據源最直白的解釋就是一個數據庫就是一個數據源,但是我這個項目由於歷史設計原因,可能會有理解上的誤導,比如在我這個項目中有一個 DataSourceName 的概念,但是這個 DataSourceName 是跟租戶名稱掛鉤的,即:

在這裏插入圖片描述

再結合相關的配置就成了這樣的對應關係:

在這裏插入圖片描述

要注意的是我們是可以配置多個 MapperScan 的,從而配置多個 basePackagessqlSessionFactoryRef。但是在多租戶的系統中其實 Mapper 只有一套,所以這個對應關係可以改一下,要弱化這裏 DataSourceName 的概念:

在這裏插入圖片描述

即所有租戶共用 MapperScan 等配置,所有的數據源都是同一級的。那麼到底走哪個數據源呢,需要將當前環境設置到 ThreadLocal 中,然後 AbstractRoutingDataSource 再基於當前環境和讀寫分離註解去選擇數據源。

定時任務

定時任務我覺得這塊也沒有設計的很好,後續會再改進。在《基於 MyBatis 實現多租戶數據隔離的實踐》也做了相關介紹,系統代碼中所有的數據已經有租戶標識 region 去數據隔離了。

循環所有租戶,每次循環將 region 租戶標識參數放入當前循環中,租戶過多可以拆分多個定時任務;

這個方案在多數據源方案中是不行的,因爲多數據源最關鍵的是多個庫,也就是說一個定時任務需要跑多個庫,即系統環境設置級別是高於合併數據方案的設置級別的。所以需要循環所有環境,每次循環在 ThreadLocal 中設置環境。

歡迎關注公衆號
​​​​​​在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章