MybatisPlus多資料來源及事務解決思路

左羽發表於2021-01-19

關於多資料來源解決方案

目前在SpringBoot框架基礎上多資料來源的解決方案大多手動建立多個DataSource,後續方案有三:

  1. 繼承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,使用AOP切面注入相應的資料來源 ,但是這種做法僅僅適用單Service方法使用一個資料來源可行,如果單Service方法有多個資料來源執行會造成誤讀。
  2. 通過DataSource配置 JdbcTemplateBean,直接使用 JdbcTemplate操控資料來源。
  3. 分別通過DataSource建立SqlSessionFactory並掃描相應的Mapper檔案和Mapper介面。

MybatisPlus

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步驟(目的就是讓JTAUserTransaction接管驅動為分散式的資料來源,通常為AtomikosDataSourceBean):

  1. 配置好AtomikosDataSourceBean
  2. AtomikosDataSourceBean交給SqlSessionFactory
  3. 配置UserTransaction事務管理。

但是我們用的是MybatisPlus,我們需要做的是接管MybatisPlus每一個資料來源的配置,然後再把資料來源依次交給MybatisPlus進行管理。

看看MybatisPlus是怎麼進行多資料來源配置的,原始碼裡有這幾個地方需要重點看一下:

  1. com.baomidou.dynamic.datasource.provider.AbstractDataSourceProvider,這個就是MybatisPlus多資料來源配置的方式,利用HashMap來裝載。
  2. 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多資料來源的誤走,且支援多資料來源下的事務問題。

做任何事情,重要的是思路,而不是搬磚。

本文首發於我的個人部落格左羽(一杯茶)

相關文章