Springboot源碼分析之事務問題

来源:https://www.cnblogs.com/qinzj/archive/2019/09/03/11456067.html
-Advertisement-
Play Games

摘要: 事務在後端開發中無處不在,是數據一致性的最基本保證。要明白進事務的本質就是進到事務切麵的代理方法中,最常見的是同一個類的非事務方法調用一個加了事務註解的方法沒進入事務。我們以 代理為例,由於Spring的對於 代理的實現,進入被代理方法的時候實際上已經離開了“代理這一層殼子”,可以認為代碼走 ...


摘要:

事務在後端開發中無處不在,是數據一致性的最基本保證。要明白進事務的本質就是進到事務切麵的代理方法中,最常見的是同一個類的非事務方法調用一個加了事務註解的方法沒進入事務。我們以cglib代理為例,由於Spring的對於cglib AOP代理的實現,進入被代理方法的時候實際上已經離開了“代理這一層殼子”,可以認為代碼走到的是一個朴素的bean,調用同一個bean中方法自然與代理沒有半毛錢關係了。
一般對於聲明式事務都是以調用另一個類的加了@Transactional註解的public方法作為入口的。

spring事務關鍵處理流程

  • EnableTransactionManagement註解導入TransactionManagementConfigurationSelector
  • TransactionManagementConfigurationSelector載入InfrastructureAdvisorAutoProxyCreator(但不一定是它,一般都是AnnotationAwareAspectJAutoProxyCreator),BeanFactoryTransactionAttributeSourceAdvisorTransactionInterceptor
    -AnnotationAwareAspectJAutoProxyCreatorioc流程一個關鍵步驟是查找Advisor,有兩個方面,第一是實現了Advisor介面的類,第二是基於註解Aspectj。關鍵是BeanFactoryTransactionAttributeSourceAdvisor被載入進了代理緩存
  • 代理調用方法的時候會執行DefaultAdvisorChainFactory#getInterceptorsAndDynamicInterceptionAdvice,這個時候就會將我們的

BeanFactoryTransactionAttributeSourceAdvisor派上用處,最主要的還是它裡面的TransactionAttributeSourcePointcut進行匹配,執行TransactionInterceptor的方法

TransactionInterceptor

    @Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
       // Work out the target class: may be {@code null}.
       // The TransactionAttributeSource should be passed the target class
       // as well as the method, which may be from an interface.
       Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    
       // Adapt to TransactionAspectSupport's invokeWithinTransaction...
       return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
    }

TransactionAspectSupport

    @Nullable
    protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
          final InvocationCallback invocation) throws Throwable {
    
       // If the transaction attribute is null, the method is non-transactional.
       TransactionAttributeSource tas = getTransactionAttributeSource();
       final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
       final PlatformTransactionManager tm = determineTransactionManager(txAttr);
       final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    
       if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
          // Standard transaction demarcation with getTransaction and commit/rollback calls.
          TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
    
          Object retVal;
          try {
             // This is an around advice: Invoke the next interceptor in the chain.
             // This will normally result in a target object being invoked.
             retVal = invocation.proceedWithInvocation();
          }
          catch (Throwable ex) {
             // target invocation exception
             completeTransactionAfterThrowing(txInfo, ex);
             throw ex;
          }
          finally {
             cleanupTransactionInfo(txInfo);
          }
          commitTransactionAfterReturning(txInfo);
          return retVal;
       }
    
       else {
          final ThrowableHolder throwableHolder = new ThrowableHolder();
    
          // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
          try {
             Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, status -> {
                TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
                try {
                   return invocation.proceedWithInvocation();
                }
                catch (Throwable ex) {
                   if (txAttr.rollbackOn(ex)) {
                      // A RuntimeException: will lead to a rollback.
                      if (ex instanceof RuntimeException) {
                         throw (RuntimeException) ex;
                      }
                      else {
                         throw new ThrowableHolderException(ex);
                      }
                   }
                   else {
                      // A normal return value: will lead to a commit.
                      throwableHolder.throwable = ex;
                      return null;
                   }
                }
                finally {
                   cleanupTransactionInfo(txInfo);
                }
             });
    
             // Check result state: It might indicate a Throwable to rethrow.
             if (throwableHolder.throwable != null) {
                throw throwableHolder.throwable;
             }
             return result;
          }
          catch (ThrowableHolderException ex) {
             throw ex.getCause();
          }
          catch (TransactionSystemException ex2) {
             if (throwableHolder.throwable != null) {
                logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                ex2.initApplicationException(throwableHolder.throwable);
             }
             throw ex2;
          }
          catch (Throwable ex2) {
             if (throwableHolder.throwable != null) {
                logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
             }
             throw ex2;
          }
       }
    }

這次在分析這個方法,但是從事務的異常,不生效等角度來分析問題。註解事務和編程式都一樣的核心思想,下麵我們來分析註解事務邏輯

    if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
                // Standard transaction demarcation with getTransaction and commit/rollback calls.
                TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
    
                Object retVal;
                try {
                    // This is an around advice: Invoke the next interceptor in the chain.
                    // This will normally result in a target object being invoked.
                    retVal = invocation.proceedWithInvocation();
                }
                catch (Throwable ex) {
                    // target invocation exception
                    completeTransactionAfterThrowing(txInfo, ex);
                    throw ex;
                }
                finally {
            // 把上一層事務的TxInfo重新綁到ThreadLocal中
                    cleanupTransactionInfo(txInfo);
                }
                commitTransactionAfterReturning(txInfo);
                return retVal;
            }

請記住這幾個核心的方法邏輯順序和異常捕獲哦!

    protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
            if (txInfo != null && txInfo.getTransactionStatus() != null) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
                            "] after exception: " + ex);
                }
          //事務回滾的異常支持
                if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
                    try {
                        txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
                    }
                    catch (TransactionSystemException ex2) {
                        logger.error("Application exception overridden by rollback exception", ex);
                        ex2.initApplicationException(ex);
                        throw ex2;
                    }
                    catch (RuntimeException | Error ex2) {
                        logger.error("Application exception overridden by rollback exception", ex);
                        throw ex2;
                    }
                }
                else {
                    // We don't roll back on this exception.
                    // Will still roll back if TransactionStatus.isRollbackOnly() is true.
                    try {
                        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
                    }
                    catch (TransactionSystemException ex2) {
                        logger.error("Application exception overridden by commit exception", ex);
                        ex2.initApplicationException(ex);
                        throw ex2;
                    }
                    catch (RuntimeException | Error ex2) {
                        logger.error("Application exception overridden by commit exception", ex);
                        throw ex2;
                    }
                }
            }
        }

事務回滾的異常支持

    @Override
    public boolean rollbackOn(Throwable ex) {
       return (ex instanceof RuntimeException || ex instanceof Error);
    }

註意點來了,僅支持運行時異常和錯誤機制,否則不予回滾。併進行直接條件。

AbstractPlatformTransactionManager

    private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
       try {
         //預設false
          boolean unexpectedRollback = unexpected;
          try {
            //回調TransactionSynchronization對象的beforeCompletion方法。
             triggerBeforeCompletion(status);
             if (status.hasSavepoint()) {
                if (status.isDebug()) {
                   logger.debug("Rolling back transaction to savepoint");
                }
                status.rollbackToHeldSavepoint();
             }
            // 在最外層事務邊界進行回滾
             else if (status.isNewTransaction()) {
                if (status.isDebug()) {
                   logger.debug("Initiating transaction rollback");
                }
               // 由具體TxMgr子類實現回滾。
                doRollback(status);
             }
             else {
                // Participating in larger transaction
                if (status.hasTransaction()) {
                  /*
                     * 內層事務被標記為rollBackOnly或者globalRollbackOnParticipationFailure開關開啟時,給當前事務標記需要回滾。
                     * 
                     * 如果內層事務顯式打上了rollBackOnly的標記,最終全事務一定是回滾掉的。
                     * 
                     * 但如果沒有被打上rollBackOnly標記,則globalRollbackOnParticipationFailure開關就很重要了。
                     * globalRollbackOnParticipationFailure開關預設是開啟的,也就是說內層事務掛了,最終的結果只能是全事務回滾。
                     * 但如果globalRollbackOnParticipationFailure開關被關閉的話,內層事務掛了,外層事務業務方法中可以根據情況控制是否回滾。
                     */
     
                   if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                      if (status.isDebug()) {
                         logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                      }
                     // 由具體TxMgr子類實現回滾。
                      doSetRollbackOnly(status);
                   }
                   else {
                      if (status.isDebug()) {
                         logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
                      }
                   }
                }
                else {
                   logger.debug("Should roll back transaction but cannot - no transaction available");
                }
                // Unexpected rollback only matters here if we're asked to fail early
                if (!isFailEarlyOnGlobalRollbackOnly()) {
                   unexpectedRollback = false;
                }
             }
          }
          catch (RuntimeException | Error ex) {
             triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
             throw ex;
          }
    // 回調TransactionSynchronization對象的afterCompletion方法。
          triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
    
          // Raise UnexpectedRollbackException if we had a global rollback-only marker
          if (unexpectedRollback) {
             throw new UnexpectedRollbackException(
                   "Transaction rolled back because it has been marked as rollback-only");
          }
       }
       finally {
          cleanupAfterCompletion(status);
       }
    }

案例分析

file

file

有經驗的同學肯定知道整個事務最終被回滾掉了, TransactionB#test並沒有執行System.out.println("TransactionB#test after");
其實對於Spring事務來說,這樣的結果是正確的,但對於開發者來說,這個結果確實看似有些“不能理解”。

我們不妨來分析一下原因:

首先TransactionB#test本身是直接拋出RuntimeException的,那麼退棧到事務切麵後,事務切麵會發現需要回滾但因為TransactionB#test還不是事務的最外層邊界,所以在AbstractPlatformTransactionManager#processRollback方法僅僅會調用doSetRollbackOnly(status);,子類DataSourceTransactionManager會拿出DefaultTransactionStatus中的transaction對象打上回滾標記,具體來說就是transaction對象(對於DataSourceTransactionManager來說類型是DataSourceTransactionObject)會取出ConnectionHolder,調用setRollbackOnly。我們知道這樣就相當於標記是一個全局的標記了,因為只要是隸屬於同一個物理事務的Spring事務都能夠讀到同一個ConnectionHolder

    protected void doSetRollbackOnly(DefaultTransactionStatus status) {
            DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)status.getTransaction();
            if (status.isDebug()) {
                this.logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + "] rollback-only");
            }
      //關鍵點
            txObject.setRollbackOnly();
        }

回到上層事務切麵,在AbstractPlatformTransactionManager#commit方法讀到if(!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly())條件成立,接下來調用processRollback,由於在事務最外層邊界會物理回滾掉,並且也正是到了事務最外層邊界,Spring拋出UnexpectedRollbackException

如何解決?

那麼問題怎麼解決呢,這個問題有好幾種解決辦法,但是得根據具體情況決定。

  • 根據實際代碼與業務情況處理,如果內嵌事務註解取消,Spring也不會拋出UnexpectedRollbackException。但是方法實際上並沒有完整執行,所以這樣的解決思路很容易導致出現不完整的臟數據。

  • 手動控制是否回滾。如果不能接受內嵌事務掛掉的話,可以在catch塊裡加上TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();用於顯式控制回滾。這樣Spring就明白你自己要求回滾事務,而不是unexpected了。Spring也不會拋出UnexpectedRollbackException了。那麼如果在上層事務中捕獲到異常,真的就是不想回滾,即便上層事務發生了異常,也想要最終提交整個事務呢?如果有這樣的需求的話,可以給事務管理器配置一個參數setGlobalRollbackOnParticipationFailure(false);

  • 如果isGlobalRollbackOnParticipationFailure為false,則會讓主事務決定回滾,如果當遇到exception加入事務失敗時,調用者能繼續在事務內決定是回滾還是繼續。然而,要註意是那樣做僅僅適用於在數據訪問失敗的情況下且只要所有操作事務能提交,這個方法也能解決,但顯然影響到全局的事務屬性,所以極力不推薦使用。

    public final void commit(TransactionStatus status) throws TransactionException {
       if (status.isCompleted()) {
          throw new IllegalTransactionStateException(
                "Transaction is already completed - do not call commit or rollback more than once per transaction");
       }
    
       DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
       if (defStatus.isLocalRollbackOnly()) {
          if (defStatus.isDebug()) {
             logger.debug("Transactional code has requested rollback");
          }
          processRollback(defStatus, false);
          return;
       }
    
       if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
          if (defStatus.isDebug()) {
             logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
          }
          processRollback(defStatus, true);
          return;
       }
    
       processCommit(defStatus);
    }

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

-Advertisement-
Play Games
更多相關文章
  • package com.atguigu;public class Fanzhuan { public static void main(String[] args) { //數組的反轉 //方法一 String[] arr=new String[]{"JJ","DD","MM","BB","GG", ...
  • Go語言中有個概念叫做goroutine, 這類似我們熟知的線程,但是更輕。 以下的程式,我們串列地去執行兩次loop函數: go package main import "fmt" func main() { loop() loop() } func loop() { for i := 0; i ...
  • 數組Array 1.數組的創建方式 字面量方式創建: 使用構造函數的方式創建(使用new關鍵字對構造函數進行創建對象) 2.數組的賦值 3.數組的常用方法 3.1 concat:把幾個數組合併成一個數組 3.2 join:將數組中的元素使用指定的字元串連接起來,他會形成一個新的字元串 3.3 將數組 ...
  • 來一道GIL面試題 描述python GIL的概念,以及他對python多線程的影響,編寫一個多線程抓取網頁的程式,並闡明多線程抓取程式是否比單線程有所提升,並解釋原因。 參考答案: 1.python語言和GIL沒有關係,僅僅是由於歷史原因在CPython虛擬機(解釋器),難以移除GIL 2.GIL ...
  • package com.atguigu;public class fuzhi { public static void main(String[] args) { int[] array1=new int[]{2,3,5,7,11,13,17,19};//靜態初始化 int[] array2; fo ...
  • 1.源文件聲明規則2.JAVA基本類型void3.數據類型預設值4.自動類型轉換5.Java變數類型6.Java局部變數7.訪問控制修飾符8.父類與子類的訪問控制9.instanceof運算符 1.源文件聲明規則 一個源文件中只能有一個public類 一個源文件中可以有多個非public類 源文件名 ...
  • 前言 ArrayList 作為 Java 集合框架中最常用的類,在一般情況下,用它存儲集合數據最適合不過。知其然知其所以然,為了能更好地認識和使用 ArrayList,本文將從下麵幾方面深入理解 ArrayList: 為什麼不用數組,用 ArrayList ArrayList 特性的源碼分析 Jav ...
  • 使用PHP 表單 表單處理: PHP超全局變數:$_GET 和 $ _POST 用於處理表單數據(form data) 表單標簽 ​ action屬性:規定表單數據提交URL ​ method屬性:規定提交時使用的HTTP方法(推薦POST) 表單元素標簽 ​ type屬性:動態定義標簽框的類型 H ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...