Spring 事務(wù)方法與非事務(wù)方法相互調(diào)用 @Transactional 注解失效不回滾的問題

項目環(huán)境 Spring Boot

下面開始問題描述,發(fā)生的過程有點長,想直接看方案的直接跳過哦~!

最近在做項目中有個業(yè)務(wù)是每天定時更新xx的數(shù)據(jù),某條記錄更新中數(shù)據(jù)出錯,不影響整體數(shù)據(jù),只需記錄下來并回滾當(dāng)條記錄所關(guān)聯(lián)的表數(shù)據(jù);好啊!

這個簡單,接到任務(wù)后,樓主我三下五除二就寫完了,由于這個業(yè)務(wù)還是有些麻煩,我就在一個service里拆成了兩個方法去執(zhí)行,一個方法(A)是查詢數(shù)據(jù)與驗證組裝數(shù)據(jù)。

推薦后臺管理開源框架,基于 Spring Boot、Spring Security、JWT 后端框架,Vue & Element 前端框架的前后端分離的用戶權(quán)限管理系統(tǒng),代碼易讀易懂、界面簡潔美觀。其核心技術(shù)采用 Spring、MyBatis、Shiro 沒有任何其它過度依賴包,下載即可運行使用。

https://gitee.com/yoodb/jing-xuan

另外一個方法(B)更新這條數(shù)據(jù)所對應(yīng)的表(執(zhí)行的時候是方法A中調(diào)用方法B);由于這個數(shù)據(jù)是循環(huán)更新,所以我想的是,一條數(shù)據(jù)更新失敗直接回滾此條數(shù)據(jù)就是,不會影響其他數(shù)據(jù),其他的照常更新,所以我就在方法B上加了事務(wù),方法A沒有加;以為很完美,自測一下正常。

ok通過,再測試一下報錯情況,是否回滾,一測沒回滾,懵圈兒?以為代碼寫錯了,改了幾處地方,再測了幾次,均沒回滾。這下是真難受了。

好啦,寫到這里相信各位看官心里肯定在嘲諷老弟了,spring的傳播機制都沒搞明白(難受)。

下面開始一步步分析解決問題:

首先我們來看下spring事務(wù)的傳播機制及原因分析;

PROPAGATION_REQUIRED – 支持當(dāng)前事務(wù),如果當(dāng)前沒有事務(wù),就新建一個事務(wù)。

這是最常見的選擇。

PROPAGATION_SUPPORTS – 支持當(dāng)前事務(wù),如果當(dāng)前沒有事務(wù),就以非事務(wù)方式執(zhí)行。

PROPAGATION_MANDATORY – 支持當(dāng)前事務(wù),如果當(dāng)前沒有事務(wù),就拋出異常。

PROPAGATION_REQUIRES_NEW – 新建事務(wù),如果當(dāng)前存在事務(wù),把當(dāng)前事務(wù)掛起。

PROPAGATION_NOT_SUPPORTED – 以非事務(wù)方式執(zhí)行操作,如果當(dāng)前存在事務(wù),就把當(dāng)前事務(wù)掛起。

PROPAGATION_NEVER – 以非事務(wù)方式執(zhí)行,如果當(dāng)前存在事務(wù),則拋出異常。

PROPAGATION_NESTED – 如果當(dāng)前存在事務(wù),則在嵌套事務(wù)內(nèi)執(zhí)行。

如果當(dāng)前沒有事務(wù),則進行與PROPAGATION_REQUIRED類似的操作。?

spring默認的是PROPAGATION_REQUIRED機制,如果方法A標注了注解@Transactional?是完全沒問題的,執(zhí)行的時候傳播給方法B,因為方法A開啟了事務(wù),線程內(nèi)的connection的屬性autoCommit=false,并且執(zhí)行到方法B時。事務(wù)傳播依然是生效的,得到的還是方法A的connection,autoCommit還是為false,所以事務(wù)生效;

反之,如果方法A沒有注解@Transactional?時是不受事務(wù)管理的,autoCommit=true,那么傳播給方法B的也為true,執(zhí)行完自動提交,即使B標注了@Transactional ;

在一個Service內(nèi)部,事務(wù)方法之間的嵌套調(diào)用,普通方法和事務(wù)方法之間的嵌套調(diào)用,都不會開啟新的事務(wù)。是因為spring采用動態(tài)代理機制來實現(xiàn)事務(wù)控制,而動態(tài)代理最終都是要調(diào)用原始對象的,而原始對象在去調(diào)用方法時,是不會再觸發(fā)代理了!

所以以上就是為什么我在沒有標注事務(wù)注解的方法A里去調(diào)用標注有事務(wù)注解的方法B而沒有事務(wù)滾回的原因。

看到這里,有的看官可能在想你在方法A上標個注解不就完了嗎?為什么非要標注在方法B上?

由于我這里是循環(huán)更新數(shù)據(jù),調(diào)用一次方法B就更新一次數(shù)據(jù),涉及到幾張表,需要執(zhí)行幾條update sql,一條數(shù)據(jù)更新失敗不影響所有數(shù)據(jù),所以說一條數(shù)據(jù)更新執(zhí)行完畢后就提交一次事務(wù),如果標注在方法A上,要所有的都執(zhí)行完畢了才提交事務(wù),這樣子是有問題滴。

下邊先上下代碼:

方法A:無事務(wù)控制

方法B:有事務(wù)控制

方法B處理失敗手動拋出異常觸發(fā)回滾:

方法A調(diào)用方法B:

從上圖可以看到,如果方法B中User更新出錯后需要回滾RedPacket數(shù)據(jù),所以User更新失敗就拋出了繼承自RuntimeException的自定義異常,并且在調(diào)用方把這個異常catch到重新拋出,觸發(fā)事務(wù)回滾,但是并沒有執(zhí)行;

解決方案

1、把方法B抽離到另外一個XXService中去,并且在這個Service中注入XXService,使用XXService調(diào)用方法B。

顯然,這種方式一點也不優(yōu)雅,且要產(chǎn)生很多冗余文件,看起來很煩,實際開發(fā)中也幾乎沒人這么做吧?反正我不建議采用此方案;

2、通過在方法內(nèi)部獲得當(dāng)前類代理對象的方式,通過代理對象調(diào)用方法B

上面說了:動態(tài)代理最終都是要調(diào)用原始對象的,而原始對象在去調(diào)用方法時,是不會再觸發(fā)代理了!

所以我們就使用代理對象來調(diào)用,就會觸發(fā)事務(wù);

綜上解決方案,我覺得第二種方式簡直方便到炸,那怎么獲取代理對象呢?這里提供兩種方式:

1、使用 ApplicationContext 上下文對象獲取該對象;

2、使用 AopContext.currentProxy() 獲取代理對象,但是需要配置exposeProxy=true

我這里使用的是第二種解決方案,具體操作如下:

springboot啟動類加上注解:@EnableAspectJAutoProxy(exposeProxy = true)

方法內(nèi)部獲取代理對象調(diào)用方法

完了后再測試,數(shù)據(jù)順利回滾,至此,問題得到解決!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容