開心一刻
昨晚,小妹跟我媽聊天
小妹:媽,跟你商量個事,我想換車,資助我點呀
媽:哎呀,你那分扣的攢一堆都夠考清華的,還換車資助點,有車開就不錯了
小妹:你要是這麼逼我,別說哪天我去學人家傍大款啊
媽:哎呀媽,你臉上那褶子比你人生規劃都清晰,咋地,大款缺地圖呀,找你?
小妹:讓我回到我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); }
通過 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); } }
大家仔細看這個方法,在 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 使用有限制條件
具體看其註釋說明,就當給你們留的家庭作業了
一定要去看,不然使用出了問題可別怪我沒提醒你們