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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...