天天看點

SpringBoot多資料源事務解決方案背景前情提要問題分析解決方案總結

背景

之前有文章提供了springboot多資料源動态注冊切換的整合方案,在後續使用過程中,發現在事務控制中有多種bug發生,決定對此問題進行分析與解決

前情提要

多資料源切換流程結構圖如下所示,包含幾個組成元素

  • 自定義的資料源配置處理,通過DruidDataSource對象動态注冊到系統中
  • 自定義資料源辨別注解與切面
  • 資料源切換時的上下文線程變量持有者
  • 自定義AbstractRoutingDataSource,實作資料源路由切換
SpringBoot多資料源事務解決方案背景前情提要問題分析解決方案總結

問題分析

在Controller加入@Transitional注解後,資料源切換會失效,隻會操作主庫,查詢資料後解決方案是将切面的Order設定為-1使之執行順序在事務控制攔截之前,修改後證明有效,但是後續再次切換别的庫或者進行主庫操作無效,拿到的connection始終是第一次切換後的庫對應的連接配接

分析代碼後發現AbstractRoutingDataSource隻負責提供getConnection這一層級,但是後續對connection的操作無法跟蹤,項目架構mybatis和jdbcTemplate混合使用,後續操作在spring層面對于事務/資料源/連接配接這三者的邏輯層面操作是相同的,jdbcTemplate代碼較為簡單,是以以此為切入點進一步分析

通過斷點調試會發現sql語句的執行最終會落到execute方法,方法中開始就是通過DataSourceUtils.getConnection擷取連接配接,這裡就是我們需要追蹤的地方,點進去發現跳轉到doGetConnection方法,這裡面就是我們需要分析的具體邏輯

SpringBoot多資料源事務解決方案背景前情提要問題分析解決方案總結
SpringBoot多資料源事務解決方案背景前情提要問題分析解決方案總結

第一行擷取的ConnectionHolder就是目前事務對應的線程持有對象,因為我們知道,事務的本質就是方法内部的sql執行時對應的是同一個資料庫connection,對于不同的嵌套業務方法,唯一相同的是目前線程ID一緻,是以我們将connection與線程綁定就可以實作事務控制

SpringBoot多資料源事務解決方案背景前情提要問題分析解決方案總結

點進getResource方法,發現dataSource是作為一個key去一個Map集合裡取出對應的contextHolder

SpringBoot多資料源事務解決方案背景前情提要問題分析解決方案總結

到這裡我們好像發現點什麼,之前對jdbcTemplatechu執行個體化設定資料源直接指派自定義的DynamicDataSource,是以在事物中每次我們擷取connection依據就是DynamicDataSource這個對象作為key,是以每次都會一樣了!!

@Bean
    public JdbcTemplate jdbcTemplate(){
        JdbcTemplate jdbcTemplate = null;
        try{
            jdbcTemplate = new JdbcTemplate(dynamicDataSource());
        }catch (Exception e){
            e.printStackTrace();
        }
        return jdbcTemplate;
    }
           

後續針對mybatis查找了相關資料,事務控制預設實作是SpringManagedTransaction,源碼檢視後發現了熟悉的DataSourceUtils.getConnection,證明我們的分析方向是正确的

SpringBoot多資料源事務解決方案背景前情提要問題分析解決方案總結

解決方案

jdbcTemplate

自定義操作類繼承jdbcTemplate重寫getDataSource,将我們擷取的DataSource這個對應的key指定到實際切換庫的資料源對象上即可

public class DynamicJdbcTemplate extends JdbcTemplate {
    @Override
    public DataSource getDataSource() {
        DynamicDataSource router =  (DynamicDataSource) super.getDataSource();
        DataSource acuallyDataSource = router.getAcuallyDataSource();
        return acuallyDataSource;
    }

    public DynamicJdbcTemplate(DataSource dataSource) {
        super(dataSource);
    }
}
           
public DataSource getAcuallyDataSource() {
        Object lookupKey = determineCurrentLookupKey();
        if (null == lookupKey) {
            return this;
        }
        DataSource determineTargetDataSource = this.determineTargetDataSource();
        return determineTargetDataSource == null ? this : determineTargetDataSource;
    }
           

mybatis

自定義事務操作類,實作Transaction接口,替換TransitionFactory,這裡的實作與網上的解決方案略有不同,網上是定義三個變量,datasource(動态資料源對象)/connection(主連接配接)/connections(從庫連接配接),但是架構需要mybatis和jdbctemplate進行統一,mybatis是從connection層面控制,jdbctemplate是從datasource層面控制,是以全部使用鍵值對存儲

public class DynamicTransaction implements Transaction {
    private final DynamicDataSource dynamicDataSource;
    private ConcurrentHashMap<String, DataSource> dataSources;
    private ConcurrentHashMap<String, Connection> connections;
    private ConcurrentHashMap<String, Boolean> autoCommits;
    private ConcurrentHashMap<String, Boolean> isConnectionTransactionals;

    public DynamicTransaction(DataSource dataSource) {
        this.dynamicDataSource = (DynamicDataSource) dataSource;
        dataSources = new ConcurrentHashMap<>();
        connections = new ConcurrentHashMap<>();
        autoCommits = new ConcurrentHashMap<>();
        isConnectionTransactionals = new ConcurrentHashMap<>();
    }

    public Connection getConnection() throws SQLException {
        String dataBaseID = DBContextHolder.getDataSource();
        if (!dataSources.containsKey(dataBaseID)) {
            DataSource dataSource = dynamicDataSource.getAcuallyDataSource();
            dataSources.put(dataBaseID, dataSource);
        }
        if (!connections.containsKey(dataBaseID)) {
            Connection connection = DataSourceUtils.getConnection(dataSources.get(dataBaseID));
            connections.put(dataBaseID, connection);
        }
        if (!autoCommits.containsKey(dataBaseID)) {
            boolean autoCommit = connections.get(dataBaseID).getAutoCommit();
            autoCommits.put(dataBaseID, autoCommit);
        }
        if (!isConnectionTransactionals.containsKey(dataBaseID)) {
            boolean isConnectionTransactional = DataSourceUtils.isConnectionTransactional(connections.get(dataBaseID), dataSources.get(dataBaseID));
            isConnectionTransactionals.put(dataBaseID, isConnectionTransactional);
        }
        return connections.get(dataBaseID);
    }


    public void commit() throws SQLException {
        for (String dataBaseID : connections.keySet()) {
            Connection connection = connections.get(dataBaseID);
            boolean isConnectionTransactional = isConnectionTransactionals.get(dataBaseID);
            boolean autoCommit = autoCommits.get(dataBaseID);
            if (connection != null && !isConnectionTransactional && !autoCommit) {
                connection.commit();
            }
        }
    }

    public void rollback() throws SQLException {
        for (String dataBaseID : connections.keySet()) {
            Connection connection = connections.get(dataBaseID);
            boolean isConnectionTransactional = isConnectionTransactionals.get(dataBaseID);
            boolean autoCommit = autoCommits.get(dataBaseID);
            if (connection != null && !isConnectionTransactional && !autoCommit) {
                connection.rollback();
            }
        }
    }

    public void close() {
        for (String dataBaseID : connections.keySet()) {
            Connection connection = connections.get(dataBaseID);
            DataSource dataSource = dataSources.get(dataBaseID);
            DataSourceUtils.releaseConnection(connection, dataSource);
        }
    }

    public Integer getTimeout() {
        return null;
    }
}
           
public class DynamicTransactionFactory extends SpringManagedTransactionFactory {
    @Override
    public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        return new DynamicTransaction(dataSource);
    }
}
           
@Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        //SpringBootExecutableJarVFS.addImplClass(SpringBootVFS.class);
        final PackagesSqlSessionFactoryBean sessionFactory = new PackagesSqlSessionFactoryBean();
        sessionFactory.setDataSource(dynamicDataSource());
        sessionFactory.setTransactionFactory(new DynamicTransactionFactory());
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mybatis/**/*Mapper.xml"));
        //關閉駝峰轉換,防止帶下劃線的字段無法映射
        sessionFactory.getObject().getConfiguration().setMapUnderscoreToCamelCase(false);
        return sessionFactory.getObject();
    }
           

事務管理器

事務中庫動态切換的問題解決了,但是隻針對了主庫事務,如果從庫操作也需要事務的特性該如何操作呢,這裡就需要在注冊資料源時針對每個資料源手動注冊一個事務管理器

主庫是固定的,可以直接在配置Bean中聲明masterTransitionManage并設定為預設

@Bean("masterTransactionManager")
    @Primary
    public DataSourceTransactionManager MasterTransactionManager() {
        return new DataSourceTransactionManager(masterDataSource());
    }
           

從庫的事務管理器我們可以拿到dataSource初始化對象,然後向Spring容器注冊單例對象

public static void registerSingletonBean(String beanName, Object singletonObject) {
        //将applicationContext轉換為ConfigurableApplicationContext
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) context;
        //擷取BeanFactory
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getAutowireCapableBeanFactory();
        if(configurableApplicationContext.containsBean(beanName)) {
            defaultListableBeanFactory.destroySingleton(beanName);
        }
        //動态注冊bean.
        defaultListableBeanFactory.registerSingleton(beanName, singletonObject);

    }
           
SpringBootBeanUtil.registerSingletonBean(key + "TransactionManager", new DataSourceTransactionManager(druidDataSource));
           

總結

繼續閱讀