踩坑之spring事務(wù),非事務(wù)方法與事務(wù)方法執(zhí)行相互調(diào)用方式
踩坑spring事務(wù),非事務(wù)方法與事務(wù)方法執(zhí)行相互調(diào)用
項(xiàng)目環(huán)境 sprinigboot
下面開始問題描述,發(fā)生的過程有點(diǎn)長,想直接看方案的直接跳過哦~
最近在做項(xiàng)目中有個(gè)業(yè)務(wù)是每天定時(shí)更新xx的數(shù)據(jù),某條記錄更新中數(shù)據(jù)出錯(cuò),不影響整體數(shù)據(jù),只需記錄下來并回滾當(dāng)條記錄所關(guān)聯(lián)的表數(shù)據(jù);
好啊,這個(gè)簡單,接到任務(wù)后,樓主我三下五除二就寫完了,由于這個(gè)業(yè)務(wù)還是有些麻煩,我就在一個(gè)service里拆成了兩個(gè)方法去執(zhí)行,一個(gè)方法(A)是查詢數(shù)據(jù)與驗(yàn)證組裝數(shù)據(jù),另外一個(gè)方法(B)更新這條數(shù)據(jù)所對應(yīng)的表(執(zhí)行的時(shí)候是方法A中調(diào)用方法B);
由于這個(gè)數(shù)據(jù)是循環(huán)更新,所以我想的是,一條數(shù)據(jù)更新失敗直接回滾此條數(shù)據(jù)就是,不會(huì)影響其他數(shù)據(jù),其他的照常更新,所以我就在方法B上加了事務(wù),方法A沒有加;
以為很完美,自測一下正常,ok通過,再測試一下報(bào)錯(cuò)情況,是否回滾,一測,沒回滾,懵圈兒?
以為代碼寫錯(cuò)了,改了幾處地方,再測了幾次,均沒回滾.這下是真難受了.
好啦,寫到這里,相信各位看官心里肯定在嘲諷老弟了,spring的傳播機(jī)制都沒搞明白(/難受)
下面開始一步步分析解決問題
首先我們來看下spring事務(wù)的傳播機(jī)制及原因分析
PROPAGATION_REQUIRED
-- 支持當(dāng)前事務(wù),如果當(dāng)前沒有事務(wù),就新建一個(gè)事務(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ù),則進(jìn)行與PROPAGATION_REQUIRED類似的操作。
spring默認(rèn)的是PROPAGATION_REQUIRED機(jī)制,如果方法A標(biāo)注了注解@Transactional 是完全沒問題的,執(zhí)行的時(shí)候傳播給方法B,因?yàn)榉椒ˋ開啟了事務(wù),線程內(nèi)的connection的屬性autoCommit=false,并且執(zhí)行到方法B時(shí),事務(wù)傳播依然是生效的,得到的還是方法A的connection,autoCommit還是為false,所以事務(wù)生效;反之,如果方法A沒有注解@Transactional 時(shí),是不受事務(wù)管理的,autoCommit=true,那么傳播給方法B的也為true,執(zhí)行完自動(dòng)提交,即使B標(biāo)注了@Transactional ;
在一個(gè)Service內(nèi)部,事務(wù)方法之間的嵌套調(diào)用,普通方法和事務(wù)方法之間的嵌套調(diào)用,都不會(huì)開啟新的事務(wù).
是因?yàn)閟pring采用動(dòng)態(tài)代理機(jī)制來實(shí)現(xiàn)事務(wù)控制,而動(dòng)態(tài)代理最終都是要調(diào)用原始對象的,而原始對象在去調(diào)用方法時(shí),是不會(huì)再觸發(fā)代理了!
所以以上就是為什么我在沒有標(biāo)注事務(wù)注解的方法A里去調(diào)用標(biāo)注有事務(wù)注解的方法B而沒有事務(wù)滾回的原因;
看到這里,有的看官可能在想,你在方法A上標(biāo)個(gè)注解不就完了嗎?為什么非要標(biāo)注在方法B上?
由于我這里是循環(huán)更新數(shù)據(jù),調(diào)用一次方法B就更新一次數(shù)據(jù),涉及到幾張表,需要執(zhí)行幾條update sql, 一條數(shù)據(jù)更新失敗不影響所有數(shù)據(jù),所以說一條數(shù)據(jù)更新執(zhí)行完畢后就提交一次事務(wù),如果標(biāo)注在方法A上,要所有的都執(zhí)行完畢了才提交事務(wù),這樣子是有問題滴.
下邊先上下代碼
方法A:無事務(wù)控制
方法B:有事務(wù)控制
方法B處理失敗手動(dòng)拋出異常觸發(fā)回滾:
方法A調(diào)用方法B:
從上圖可以看到,如果方法B中User更新出錯(cuò)后需要回滾RedPacket數(shù)據(jù),所以User更新失敗就拋出了繼承自RuntimeException的自定義異常,并且在調(diào)用方把這個(gè)異常catch到重新拋出,觸發(fā)事務(wù)回滾,但是并沒有執(zhí)行;
下面是解決方案
1.把方法B抽離到另外一個(gè)XXService中去,并且在這個(gè)Service中注入XXService,使用XXService調(diào)用方法B;
顯然,這種方式一點(diǎn)也不優(yōu)雅,且要產(chǎn)生很多冗余文件,看起來很煩,實(shí)際開發(fā)中也幾乎沒人這么做吧?.
反正我不建議采用此方案;
2.通過在方法內(nèi)部獲得當(dāng)前類代理對象的方式,通過代理對象調(diào)用方法B
上面說了:動(dòng)態(tài)代理最終都是要調(diào)用原始對象的,而原始對象在去調(diào)用方法時(shí),是不會(huì)再觸發(fā)代理了!
所以我們就使用代理對象來調(diào)用,就會(huì)觸發(fā)事務(wù);
綜上解決方案,我覺得第二種方式簡直方便到炸. 那怎么獲取代理對象呢?
這里提供兩種方式:
- 1.使用 ApplicationContext 上下文對象獲取該對象;
- 2.使用 AopContext.currentProxy() 獲取代理對象,但是需要配置exposeProxy=true
我這里使用的是第二種解決方案,具體操作如下:
springboot啟動(dòng)類加上注解:
@EnableAspectJAutoProxy(exposeProxy = true)
方法內(nèi)部獲取代理對象調(diào)用方法
完了后再測試,數(shù)據(jù)順利回滾,至此,問題得到解決!
都是事務(wù)這塊兒基礎(chǔ)太差的錯(cuò)啊~~
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
使用IDEA創(chuàng)建SpringBoot項(xiàng)目
本文詳細(xì)介紹了使用SpringBoot創(chuàng)建項(xiàng)目,包含配置、啟動(dòng)、開發(fā)環(huán)境配置等,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-12-12springboot項(xiàng)目編寫發(fā)送異常日志到企微工具包的操作方法
本文介紹了Springboot項(xiàng)目如何編寫發(fā)送異常日志到企業(yè)微信的工具包,內(nèi)容包括創(chuàng)建基礎(chǔ)Bean、配置類、pom依賴等步驟,并展示了如何通過nacos進(jìn)行配置,這為開發(fā)者提供了一種有效的日志管理方案,方便快速定位和處理項(xiàng)目中的異常問題,感興趣的朋友跟隨小編一起看看吧2024-09-09使用Sentinel自定義返回和實(shí)現(xiàn)區(qū)分來源方式
這篇文章主要介紹了使用Sentinel自定義返回和實(shí)現(xiàn)區(qū)分來源方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04