關於多資料來源解決方案
目前在SpringBoot
框架基礎上多資料來源的解決方案大多手動建立多個DataSource
,後續方案有三:
- 繼承
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
,使用AOP
切面注入相應的資料來源 ,但是這種做法僅僅適用單Service
方法使用一個資料來源可行,如果單Service
方法有多個資料來源執行會造成誤讀。 - 通過
DataSource
配置JdbcTemplate
Bean,直接使用JdbcTemplate
操控資料來源。 - 分別通過
DataSource
建立SqlSessionFactory
並掃描相應的Mapper
檔案和Mapper
介面。
MybatisPlus
的多資料來源
我通過閱讀原始碼,發現MybatisPlus
的多資料來源解決方案正是AOP
,繼承了org.springframework.jdbc.datasource.AbstractDataSource
,有自己對ThreadLocal
的處理。通過註解切換資料來源。也就是說,MybatisPlus
只支援在單Service
方法內操作一個資料來源,畢竟官網都指明——“強烈建議只註解在service實現上”。
而後,注意看com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder
,也就是MybatisPlus
是如何切換資料來源的。
重點看:
/**
* 為什麼要用連結串列儲存(準確的是棧)
* <pre>
* 為了支援巢狀切換,如ABC三個service都是不同的資料來源
* 其中A的某個業務要調B的方法,B的方法需要呼叫C的方法。一級一級呼叫切換,形成了鏈。
* 傳統的只設定當前執行緒的方式不能滿足此業務需求,必須模擬棧,後進先出。
* </pre>
*/
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new ThreadLocal() {
@Override
protected Object initialValue() {
return new ArrayDeque();
}
};
這段話翻譯為大家都能懂得的意思就是“可以同時操控多個資料來源”。那麼,在MYSQL
中,有語法為schemaName
+.
+tableName
,如此一來就不會誤走資料來源了。
我繼續看MybatisPlus
是如何利用mybatis
本身的ORM
機制將實體類自動對映以及生成SQL
語句的(這裡插一句,MybatisPlus
的原始碼易讀懂,寫的很不錯)。無意看到了註解com.baomidou.mybatisplus.annotation.TableName
中的schema
,如果在類上加schema
,在生成SQL
語句時就會生成schemaName
+.
+tableName
格式。
MybatisPlus
多資料來源事務(JTA
)
簡單說明一下JTA
JTA
包括事務管理器(Transaction Manager)和一個或多個支援 XA 協議的資源管理器 ( Resource Manager ) 兩部分, 可以將資源管理器看做任意型別的持久化資料儲存;事務管理器則承擔著所有事務參與單元的協調與控制。
JTA
只是提供了一個介面,並沒有提供具體的實現。
不過Atomikos
對其進行了實現,而後SpringBoot
將其進行了整合,對其進行了託管,很方便開發者拿來即用。
其中事務管理器的主要部分為UserTransaction
介面,開發人員通過此介面在資訊系統中實現分散式事務;而資源管理器則用來規範提供商(如資料庫連線提供商)所提供的事務服務,它約定了事務的資源管理功能,使得 JTA
可以在異構事務資源之間執行協同溝通。
通常接入JTA
步驟(目的就是讓JTA
的UserTransaction
接管驅動為分散式的資料來源,通常為AtomikosDataSourceBean
):
- 配置好
AtomikosDataSourceBean
。 - 把
AtomikosDataSourceBean
交給SqlSessionFactory
。 - 配置
UserTransaction
事務管理。
但是我們用的是
MybatisPlus
,我們需要做的是接管MybatisPlus
每一個資料來源的配置,然後再把資料來源依次交給MybatisPlus
進行管理。
看看MybatisPlus
是怎麼進行多資料來源配置的,原始碼裡有這幾個地方需要重點看一下:
com.baomidou.dynamic.datasource.provider.AbstractDataSourceProvider
,這個就是MybatisPlus
多資料來源配置的方式,利用HashMap
來裝載。com.baomidou.dynamic.datasource.DynamicDataSourceCreator
,這個是每個資料來源的配置方式。
其中com.baomidou.dynamic.datasource.provider.AbstractDataSourceProvider
實現了介面com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider
,是該介面的預設的實現。也就是說我們只需要實現該介面,自己配置多資料來源以及每個資料來源的驅動,成為該介面的預設實現就OK。
-
實現該介面,配置多資料來源:
package xxx.xxx.xxx.config; import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider; import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * @author : zuoyu * @description : 接管MybatisPlus多資料來源至Atomikos管理 * @date : 2020-06-01 16:36 **/ @Service @Primary public class DynamicDataSourceProviderImpl implements DynamicDataSourceProvider { /** * 配置檔案資料的鬆散繫結 */ private final DynamicDataSourceProperties properties; /** * Atomikos驅動資料來源建立 */ private final AtomikosDataSourceCreator atomikosDataSourceCreator; public DynamicDataSourceProviderImpl(DynamicDataSourceProperties properties, AtomikosDataSourceCreator atomikosDataSourceCreator) { this.properties = properties; this.atomikosDataSourceCreator = atomikosDataSourceCreator; } @Override public Map<String, DataSource> loadDataSources() { Map<String, DataSourceProperty> dataSourcePropertiesMap = properties.getDatasource(); Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2); for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) { String pollName = item.getKey(); DataSourceProperty dataSourceProperty = item.getValue(); dataSourceProperty.setPollName(pollName); dataSourceMap.put(pollName, atomikosDataSourceCreator.createDataSource(dataSourceProperty)); } return dataSourceMap; } }
-
Atomikos
驅動資料來源建立:package xxx.xxx.xxx.config; import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import org.springframework.stereotype.Component; import javax.sql.DataSource; /** * @author : zuoyu * @description : 事務資料來源 * @date : 2020-06-01 17:30 **/ @Component public class AtomikosDataSourceCreator { /** * 建立資料來源 * * @param dataSourceProperty 資料來源資訊 * @return 資料來源 */ public DataSource createDataSource(DataSourceProperty dataSourceProperty) { MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource(); mysqlXaDataSource.setUrl(dataSourceProperty.getUrl()); mysqlXaDataSource.setPassword(dataSourceProperty.getPassword()); mysqlXaDataSource.setUser(dataSourceProperty.getUsername()); AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); xaDataSource.setXaDataSource(mysqlXaDataSource); xaDataSource.setMinPoolSize(5); xaDataSource.setBorrowConnectionTimeout(60); xaDataSource.setMaxPoolSize(20); xaDataSource.setXaDataSourceClassName(dataSourceProperty.getDriverClassName()); xaDataSource.setTestQuery("SELECT 1 FROM DUAL"); xaDataSource.setUniqueResourceName(dataSourceProperty.getPollName()); return xaDataSource; } }
-
配置
JTA
事務管理器:package xxx.xxx.xxx.config; import com.atomikos.icatch.jta.UserTransactionImp; import com.atomikos.icatch.jta.UserTransactionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.jta.JtaTransactionManager; import javax.transaction.TransactionManager; import javax.transaction.UserTransaction; /** * @author : zuoyu * @description : 分散式事務配置 * @date : 2020-06-01 17:55 **/ @Configuration @EnableTransactionManagement public class TransactionManagerConfig { @Bean(name = "userTransaction") public UserTransaction userTransaction() throws Throwable { UserTransactionImp userTransactionImp = new UserTransactionImp(); userTransactionImp.setTransactionTimeout(10000); return userTransactionImp; } @Bean(name = "atomikosTransactionManager") public TransactionManager atomikosTransactionManager() throws Throwable { UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setForceShutdown(false); return userTransactionManager; } @Bean(name = "transactionManager") @DependsOn({"userTransaction", "atomikosTransactionManager"}) public PlatformTransactionManager transactionManager() throws Throwable { return new JtaTransactionManager(userTransaction(), atomikosTransactionManager()); } }
如此,即可
這樣一來便可解決MybatisPlus
多資料來源的誤走,且支援多資料來源下的事務問題。
做任何事情,重要的是思路,而不是搬磚。
本文首發於我的個人部落格左羽(一杯茶)