SpringBoot多數據源事務解決方案

来源:https://www.cnblogs.com/yanpeng19940119/archive/2022/05/01/16212795.html
-Advertisement-
Play Games

背景 之前有文章提供了springboot多數據源動態註冊切換的整合方案,在後續使用過程中,發現在事務控制中有多種bug發生,決定對此問題進行分析與解決 前情提要 多數據源切換流程結構圖如下所示,包含幾個組成元素 自定義的數據源配置處理,通過DruidDataSource對象動態註冊到系統中 自定義 ...


背景

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

前情提要

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

  • 自定義的數據源配置處理,通過DruidDataSource對象動態註冊到系統中

  • 自定義數據源標識註解與切麵

  • 數據源切換時的上下文線程變數持有者

  • 自定義AbstractRoutingDataSource,實現數據源路由切換

問題分析

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

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

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

第一行獲取的ConnectionHolder就是當前事務對應的線程持有對象,因為我們知道,事務的本質就是方法內部的sql執行時對應的是同一個資料庫connection,對於不同的嵌套業務方法,唯一相同的是當前線程ID一致,所以我們將connection與線程綁定就可以實現事務控制

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

到這裡我們好像發現點什麼,之前對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,證明我們的分析方向是正確的

解決方案

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));

在使用時只要對@Transitional註解指定transitionFactory名字即可

總結

解決這個問題花費了三天的時間,查了很多資料和解決方案,很多都是只有參考性或者特異性的,所以還是需把握問題的核心加上部分源碼的追蹤,比如本文中需要清晰的認識到Transition-Connection-LocalThread三者的關聯關係,才能找對排查的方向

後續實現了集成基於JMS(atomikos)的XA兩段式提交的全局事務,使用DruidXADataSrouce出現了druid和atomikos兩者線程池交互出現泄露的情況放棄了,給小伙伴們避個坑


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 經常看到有人說什麼值傳遞、引用傳遞,其實都是值傳遞,區別不過是傳的值的類型罷了。 傳值方式 java傳值有且只有一種方式,將參數的“值”複製後傳入,這個“值”是指變數名所對應的地址中存放的值,對於值類型和對象類型,由於地址中存放的東西不同,因此表現有所不同: 對於8種值類型,其存放的就是本身的值,因 ...
  • 訪問許可權修飾符: public 修飾class,方法,變數; 所修飾類的名字必須與文件名相同,文件中最多能有一個pulic修飾的類。 private class不可用,方法,變數可以用; 只限於本類成員訪問和修改,本類和子類的對象實例都不能訪問。 protected class不可用,成員(方法&變 ...
  • Tkinter組件 § Label 描述:標簽控制項,可以顯示文本和點陣圖。 語法: master:框架的父容器 option:可選項,即該標簽的可設置的屬性。這些選項可以用鍵=值的形式設置,並以逗號分隔。 序號|可選項 & 描述 : |: 1 | anchor 文本或圖像在背景內容區的位置,預設為 c ...
  • 今天一大早,群里(點擊加群)有小伙伴問了這樣的一個問題: 在我們使用IDEA開發項目的時候,通常都會有很多配置項需要去設置,比如對於Java項目來說,一般就包含:JDK配置、Maven配置等。那麼如果想要設置一個預設的項目配置的話,要如何做呢? 先來找到入口,在File菜單中找到New Projec ...
  • Pandas 是 Python 語言的一個擴展程式庫,用於數據分析。 Pandas 是一個開放源碼、BSD 許可的庫,提供高性能、易於使用的數據結構和數據分析工具。 Pandas 名字衍生自術語 "panel data"(面板數據)和 "Python data analysis"(Python 數據 ...
  • A benchmark is a test of the performance of a computer system. ​ 基準測試是對電腦系統的性能的測試 計時器 性能的指標就是時間,在c++11後計時十分方便,因為有<chrono>神器 在性能測試中,一般依賴堆棧上的生命周期來進行計時 ...
  • JUC學習 1.什麼是JUC java.util 工具包、包、分類 業務:普通的線程代碼 Thread Runnable 沒有返回值、效率相比入 Callable 相對較低! 2.線程和進程 線程、進程,如果不能使用一句話說出來的技術,不扎實! 進程:一個程式,QQ.exe Music.exe 程式 ...
  • 拓撲排序 簡介 拓撲排序是將偏序的數據線性化的一種排序方法。複習下偏序和全序的概念: 全序關係是偏序關係的一個子集。 全序是集合內任何一對元素都是可比較的,比如數軸上的點都具有一個線性的數值,因此根據數值就可以進行比較。 偏序是集合內不是所有元素都是可以比較的,比如平面內的點由橫坐標和縱坐標組成,是 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...