事務提交之後再執行某些操作 → 引發對 TransactionSynchronizationManager 的探究

開心一刻

  昨晚,小妹跟我媽聊天

  小妹:媽,跟你商量個事,我想換車,資助我點呀

  媽:哎呀,你那分扣的攢一堆都夠考清華的,還換車資助點,有車開就不錯了

  小妹:你要是這麼逼我,別說哪天我去學人家傍大款啊

  媽:哎呀媽,你臉上那褶子比你人生規劃都清晰,咋地,大款缺地圖呀,找你?

  小妹:讓我回到我18歲,大個、水靈、白,你再看看

  媽:你18長的像黑魚棒似的,還水靈白,消防栓水靈,也沒見誰娶它呀,女人吶,你得有內涵

前情回顧

  在記一次線上問題 → 偶爾的熱情真的難頂呀!

  我們知道了女神偶爾的消息可能是借錢

  那你到底是借還是不借?

  不好意思,貌似抓錯重點了

  重點應該是:把消息發送從事務中拎出來就好了,也就是等事務提交後,再發消息

  什麼,沒看記一次線上問題 → 偶爾的熱情真的難頂呀!,不知道重點,那還不趕緊去看?

  我光提了重點,但是沒給你們具體實現,就問你們氣不氣?

  本着認真負責的態度,我還是提供幾種實現,誰讓我太寵你們了

事務拎出來

  說起來很簡單,做起來其實也很簡單

  犯病拎

  爲了更接近真實案例,我把

  調整一下

   User更新 和 插入操作日誌 在一個事務中, 發消息 需要拎出去

  拎出去還不簡單,看我表演

  相信大家都能看懂如上代碼,上游調用 update 的地方也不用改,簡直完美!

  大家看仔細了, update 上的 @Transactional(rollbackFor = Exception.class) 被拿掉了,不是漏寫了!

  如果 update 上繼續保留 @Transactional(rollbackFor = Exception.class) 

  是什麼情況?

  那不是和沒拎出來一樣了嗎?特麼的還多寫了幾行代碼!

  回到剛拎出來的情況, update 和 updateUser 在同一個類中,非事務方法 update 調用了事務方法 updateUser ,事務會怎麼樣?

  如果你還沒反應過來,八股文需要再背一背了:在同一個類中,一個非事務方法調用另一個事務方法,事務不會生效

  恭喜你,解決一個 bug 的同時,成功引入了另一個 bug 

  你懵的同時,你老大也懵

  你們肯定會問:非事務方法 update 調用事務方法 updateUser ,事務爲什麼會失效了?

  巧了,正好我有答案:記一次線上問題 → 事務去哪了

  彆扭拎

  同一個類中,非事務方法調用事務方法,事務不生效的解決方案中,是不是有這樣一種解決方案:自己註冊自己

  我們 debug 一下,看下堆棧情況

  我們先看 update 

  調用鏈中沒有事務相關內容

  我們再看 updateUser 

  調用鏈中有事務相關內容

  從結果來看,確實能夠滿足要求,上游調用 update 的地方也不用調整,並且還自給自足,感覺是個好方案呀

  但 自己註冊自己 這種情況,你們見得多嗎,甚至見過嗎

  反正我看着好彆扭,不知道你們有這種感覺沒有?

  要不將就着這麼用?

  常規拎

   自己註冊自己 是非常不推薦的!

  爲什麼不推薦? 來來來,把臉伸過來

  怎麼這麼多問題,非要把我榨乾?

  那我就說幾點

  1、違反了單一職責原則,一個類應該只負責一件事情,如果它開始依賴自己,那麼它的職責就不夠清晰,這可能會導致代碼難以維護和擴展

  2、循環依賴,自己依賴自己就是最簡單版的循環依賴,雖說 Spring 能解決部分循環依賴,但 Spring 是不推薦循環依賴寫法的

  3、導致一些莫名其妙的問題,還非常難以排查,大家可以 Google 一下,關鍵字類似: Spring 自己注入自己 有什麼問題 

  推薦的做法是新建一個 UserManager ,類似如下

  此時,上游調用的地方也需要調整,改調用 com.qsl.manager.UserManager#update ,如下所示:

  同樣 debug 下,來看看堆棧信息

   com.qsl.manager.UserManager#update 調用棧情況如下

  非常簡單,沒有任何的代理

  我們再看下 com.qsl.service.impl.UserServiceImpl#updateUser 

  此時,調用鏈中是有事務相關內容的

  是不是很完美的將消息發送從事務中抽出來了?

  這確實也是我們最常用的方式,沒有之一!

  驚喜拎

  既不想新增 UserManager ,又想把消息發送從事務中抽離出來,還要保證事務生效,並且不能用 自己註冊自己 ,有什麼辦法嗎

  好處全都要,壞處往外撂,求求你,做個人吧


  但是,注意轉折來了!

  最近我還真學了一個新知識: TransactionSynchronizationManager ,發現它完美契合上述的既要、又要、還要、並且要

  我們先回到最初的版本

  接下來看我表演,稍微調整下代碼

  什麼,調整了哪些,看的不夠直觀?

  我真是服了你們這羣老六,那我就再愛你們一次,讓你們看的更直觀,直接 beyond compare 下

  就調整這麼一點,上游調用 update 的地方也不用調整,你們的既要、又要、還要、並且要就滿足了!

  是不是很簡單?

  爲了嚴謹,我們來驗證一下

  如何驗證了?

  最簡單的辦法就是在發送消息的地方打個斷點,如下所示

  當 debug 執行到此的時候,消息是未發送的,這個沒問題吧?

  那麼我們只需要驗證:此時事務是否已經提交

  問題又來了,如何驗證事務已經提交了呢?

  很簡單,我們直接去數據庫查對應的記錄,是不是修改之後的數據,如果是,那就說明事務已經提交,否則說明事務沒提交,能理解吧?

  我們以修改 張三 的密碼爲例, bebug 未開始,此時 張三 的密碼是 zhangsan1 

  我們把 張三 的密碼改成 zhangsan2 

  開始 bebug 

  此時,消息還未發送,我們去數據庫查下 張三 的密碼

  此時 張三 的密碼已經是 zhangsan2 了,是修改之後的數據,說明了什麼?

  說明事務已經提交了,而此時消息還未發送!
  是不是很優雅的實現了最初的重點:把消息發送從事務中拎出來就好了,也就是等事務提交後,再發消息

TransactionSynchronizationManager

  從字面意思來看,就是一個事務同步管理器

  概況

   TransactionSynchronizationManager 是 Spring 框架中提供的一個工具類,主要用於管理事務的同步操作

  通過 TransactionSynchronizationManager ,開發者可以自定義實現 TransactionSynchronization 接口或繼承 TransactionSynchronizationAdapter 

  從而在事務的不同階段(如提交前、提交後、回滾後等)執行特定的操作(如發送消息)

   TransactionSynchronizationManager 提供了很多靜態方法, registerSynchronization 就是其中之一(其他的大家自行去學習)

  入參類型是 TransactionSynchronization ,該接口定義了幾個事務同步方法(命名很好,見名知意)

  分別代表着在事務的不同階段,會被執行的操作,比如 afterCommit 會在事務提交後執行

  底層原理

  爲什麼事務提交後一定會執行 org.springframework.transaction.support.TransactionSynchronization#afterCommit ?

  幕後一定有操盤手,我們來揪一揪它

  怎麼揪?

  正所謂: 源碼之下無密碼 ,我們直搗黃龍幹源碼

  問題又來了, Spring 源碼那麼多,我們怎麼知道哪一部分跟 TransactionSynchronization 有關?

  很簡單,去 bebug 的堆棧中找,很容易就能找到切入點

  切入點是不是很明顯了: org.springframework.transaction.support.AbstractPlatformTransactionManager#commit 

/**
 * This implementation of commit handles participating in existing
 * transactions and programmatic rollback requests.
 * Delegates to {@code isRollbackOnly}, {@code doCommit}
 * and {@code rollback}.
 * @see org.springframework.transaction.TransactionStatus#isRollbackOnly()
 * @see #doCommit
 * @see #rollback
 */
@Override
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);
}
View Code

  通過 commit 的源碼,或者上圖的調用鏈,我們會繼續來到 org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit 

/**
 * Process an actual commit.
 * Rollback-only flags have already been checked and applied.
 * @param status object representing the transaction
 * @throws TransactionException in case of commit failure
 */
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
    try {
        boolean beforeCompletionInvoked = false;

        try {
            boolean unexpectedRollback = false;
            prepareForCommit(status);
            triggerBeforeCommit(status);
            triggerBeforeCompletion(status);
            beforeCompletionInvoked = true;

            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Releasing transaction savepoint");
                }
                unexpectedRollback = status.isGlobalRollbackOnly();
                status.releaseHeldSavepoint();
            }
            else if (status.isNewTransaction()) {
                if (status.isDebug()) {
                    logger.debug("Initiating transaction commit");
                }
                unexpectedRollback = status.isGlobalRollbackOnly();
                doCommit(status);
            }
            else if (isFailEarlyOnGlobalRollbackOnly()) {
                unexpectedRollback = status.isGlobalRollbackOnly();
            }

            // Throw UnexpectedRollbackException if we have a global rollback-only
            // marker but still didn't get a corresponding exception from commit.
            if (unexpectedRollback) {
                throw new UnexpectedRollbackException(
                        "Transaction silently rolled back because it has been marked as rollback-only");
            }
        }
        catch (UnexpectedRollbackException ex) {
            // can only be caused by doCommit
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
            throw ex;
        }
        catch (TransactionException ex) {
            // can only be caused by doCommit
            if (isRollbackOnCommitFailure()) {
                doRollbackOnCommitException(status, ex);
            }
            else {
                triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
            }
            throw ex;
        }
        catch (RuntimeException | Error ex) {
            if (!beforeCompletionInvoked) {
                triggerBeforeCompletion(status);
            }
            doRollbackOnCommitException(status, ex);
            throw ex;
        }

        // Trigger afterCommit callbacks, with an exception thrown there
        // propagated to callers but the transaction still considered as committed.
        try {
            triggerAfterCommit(status);
        }
        finally {
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
        }

    }
    finally {
        cleanupAfterCompletion(status);
    }
}
View Code

  大家仔細看這個方法,在 doCommit(status) 之前有 triggerBeforeCommit(status) 、 triggerBeforeCompletion(status) 

   doCommit(status) 之後有 triggerAfterCommit(status) 、 triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED) 

  這幾個方法的作用很明顯了吧( trigger 是觸發的意思)

  接下來我們跟哪個方法?

  很明顯,我們要跟 triggerAfterCommit(status) ,因爲我們要找的是 afterCommit 的操盤手

  內容很簡單,下一步跟的對象也很明確

  這裏要分兩步說明下

  1、 TransactionSynchronizationManager.getSynchronizations() 

  先獲取所有的事務同步器,然後進行排序

  排序先撇開,我們先看看獲取到了哪些事務同步器

  第一個不眼熟,我們先不管

  第二個眼不眼熟?是不是就是 com.qsl.service.impl.UserServiceImpl#update 中的匿名內部類?(如果想看的更明顯,就不要用匿名內部類)

  是不是就對應上了:先註冊,再獲取,最後被調用

  被調用就是下面的第 2 步

  2、 invokeAfterCommit 

  邏輯很簡單,遍歷所有事務同步器,逐個調用事務同步器的 afterCommit 方法

  我們案例中的 發消息 就是在此處被執行了

  至此,相信大家都沒疑惑了吧

總結

  1、關於 Spring 循環依賴,大家可以翻閱下我之前的博客

    Spring 的循環依賴,源碼詳細分析 → 真的非要三級緩存嗎

    再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

    三探循環依賴 → 記一次線上偶現的循環依賴問題

    四探循環依賴 → 當循環依賴遇上 BeanPostProcessor,愛情可能就產生了!

    總之一句話:一定要杜絕循環依賴!

  2、事務提交之後再執行某些操作的實現方式

    事務失效的方式,大家一定要警惕,這坑很容易掉進去

    自己註冊自己的方式,直接杜絕,就當沒有這種方式

     Manager 方式很常規,可以使用

     TransactionSynchronizationManager 方式很優雅,推薦使用

    看了這篇博客後,該用哪種方式,大家心裏有數了吧

  3、TransactionSynchronizationManager 使用有限制條件

    具體看其註釋說明,就當給你們留的家庭作業了

    一定要去看,不然使用出了問題可別怪我沒提醒你們

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章