­

MybatisPlus多數據源及事務解決思路

關於多數據源解決方案

目前在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多數據源的誤走,且支持多數據源下的事務問題。

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

本文首發於我的個人博客左羽(一杯茶)

Tags: