@Transactional失效問題

作者簡介:大家好,我是smart哥,前中興通訊、美團架構師,現某互聯網公司CTO

聯系qq:184480602,加我進群,大家一起學習,一起進步,一起對抗互聯網寒冬

關于@Transactional

日常做項目時,一般情況下Service方法中如果有多個增刪改方法的調用,我們會在該業務方法上加@Transactional從而保證事務的執行(SpringBoot自動裝配默認開啟事務管理,無需@EnableTransactionManagement):

這段代碼沒太多意義,就是更新一個User的同時,更新另一個。

@Transactional注解有多個屬性可以設置,實際開發中比較常用的有兩個:

  • propagation:用于指定事務傳播行為
  • rollbackFor:用于指定能夠觸發事務回滾的異常類型,可以指定多個異常類型

這篇文章還不錯,可以看完后再回來:總結6種@Transactional注解的失效場景

對于propagation屬性,Spring提供了一個枚舉類方便我們指定事務傳播行為的類型:

特別注意,@Transactional默認的事務傳播行為是Propagation.REQUIRED,所以上面的updateUser()我只指定了rollbackFor。

上面文章提到的6種情況里,一般來說可能犯錯誤的就以下2種:

  • 同一個類中方法調用,導致@Transactional失效
  • 異常被你的catch“吃了”導致@Transactional失效

對于第2種情況,我的處理辦法是盡量不在Service層直接try catch,而是習慣拋出業務異常,讓@RestControllerAdvise統一捕獲并返回給前端。

但對于第1種情況,怎么處理呢?畢竟實際開發中,有時確實可能一不小心就發生同一個類的方法互調,此時如何解決事務失效問題呢?

發現問題

請觀察下方截圖中的代碼,不用在意具體的上下文:

  • selectUser()不加事務控制,但調用了updateUser()
  • updateUser加了事務控制,調用了兩次userMapper.update(),中間會拋出“除零異常”

selectUser()不夠貼切,名字隨便取的,請把它當做一個沒有事務的增刪改方法

在test方法中調用:

測試前數據庫記錄:

測試結果:

這證明了同一個類中的非事務方法調用事務方法確實會導致事務失效(如果事務沒失效,應該會回滾,16不會被修改)。

解決問題

方法1:給selectUser()加上@Transactional

事務確實控制住了:

方法2:ApplicationContext獲取代理對象

同一個類中非事務方法調用事務方法導致事務失效的根本原因在于,非事務方法中調用updateUser()本質上就是this.updateUser(),而this并不是代理對象,而是普通對象(后面再解釋)。

知道原因后就很好解決了:

先在selectUser()內部獲取UserService的代理對象,再通過代理對象調用updateUser()即可

方法3:注入自身

由于Spring已經替我們解決了循環依賴的問題,所以AService可以注入AService自身。

比如:

@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserService userService
}

方法4:AopContext.currentProxy()獲取代理對象

原理同上,本質是也是在selectUser()方法中獲取代理對象。不過這個方法需要額外做2步:

  • 引入aop依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • 添加注解

AopContext可以通過當前線程ThreadLocal得到代理對象。

關于代理對象與this

最后分別解釋一下上面三種辦法為什么能解決事務失效的問題,其中方法2和3的原理是一樣的。

先看方法1:給selectUser()加上@Transactional

我們原先觀察問題的角度是:selectUser()調用updateUser(),會導致updateUser()事務失效。一般來說,正向思維是想辦法讓updateUser()事務起效,但方法1卻采用了逆向思維:讓selectUser()的事務起效,從而把updateUser()放在一個更大的事務中,最終控制事務。

也就是說,它并沒有解決updateUser()事務失效的問題,內部其實還是this.updateUser(),是普通方法調用。之所以最終看起來好像事務控制成功,是因為updateUser()內部的異常沿著方法調用鏈向上拋,到了selectUser()這里觸發了回滾。

講完了方法1起效的本質后,我們再來聊聊為什么userService.selectUser()在調用時明明是代理對象:

怎么到了selectUser()內部時,this就成普通對象了呢:

請注意,即使我現在在selectUser()上加了@Transactional注解,里面的this還是普通對象。也印證了我上面的觀點:方法1并沒有解決updateUser()事務失效的問題,因為它還是用this普通對象調用updateUser(),并不會觸發事務控制。

總而言之,此時this != userService。是不是覺得很不可思議?

Why?

這要從動態代理的底層原理說起(請參考之前動態代理相關的文章),簡而言之就是下面這幅圖:

動態代理的原理是,我們可以在InvocationHandler的invoke()方法中使用target目標對象調用目標方法,最終得到的效果和靜態代理是一樣的:

所以在add()方法里使用this,其實得到的是target,也就是目標對象,而不是代理對象。

Spring自動注入時,其實是把代理對象注入到每一個@Autowired private UserService userService中。我們在Controller調用userService代理對象的add()方法時,最終會轉到目標對象的add()方法。

講完上面方法1的原理,方法2和方法3就無需多言了吧。只不過方法3得到代理對象的方式有點奇特:

最后的最后,在討論事務控制是否起效時,本文的一切論點都是基于以下2點:

  • 首先,要是代理對象
  • 其次,方法上要有@Transactional(或者xml配置形式)

至于為什么代理對象的方法上加了@Transactional就會觸發事務,需要去看Spring的AOP源碼,里面涉及到了責任鏈模式和遞歸算法。大體思路是:

0.在Spring AOP的世界里,一個個增強方法(增強代碼)會被包裝成一個個攔截器,放在攔截器鏈中。

1.代理對象調用每個方法時,其實最終都會被導向一個叫CglibAopProxy.intercept()的方法,而這個方法會判斷當前方法有沒有需要執行的攔截器鏈chain。

簡單來說就是:

// 獲取攔截器鏈if(chain.isEmpty() && Modifier.isPublic(method.getModifiers())){// 執行目標方法
} else {// 走攔截器鏈...
}

點進去else分支的代碼,會看到:

“方法為public”時才會返回methodProxy,也能被代理。也驗證了@Transactional失效的另一個情況:方法不為public時,@Transactional失效。

2.當public方法加了@Transactional,事務控制的代碼就會被加入到攔截器鏈中,最終就會出現在事務方法的前后調用。

特別要注意,任何Java代碼層面的事務控制其實還是依賴于setAutoCommit(false),也就是先關閉默認提交,此時MySQL底層就會通過日志把一連串操作先記錄起來,最后一起提交。如果中間失敗了,仍可根據日志回滾。具體實現細節可以去查閱MySQL事務相關資料。

另外大家可以關注下上面invokeWithinTransaction()的第二行代碼,里面有一句

tas.getTransactionAttribute(method, targetClass)

本質就是傳入當前事務方法和Class對象,讀取上面@Transactional的注解屬性,比如我們對rollbackFor和propagation的設置。

然后再往下會調用

TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

傳入一些參數判斷決定是否真的開啟事務(名字很形象,createTransactionIfNecessary),如果我們沒有使用@Transactional,就不會開啟事務了。

重新理解rollbackFor和propagation

相信大家以前也看了很多類似的文章,但是看完就忘了。既然花了時間,肯定還是希望能一勞永逸。所以本文也不打算這么蜻蜓點水般結束,而是來個回馬槍,和大家一起重新看看這兩個屬性,相信理解會更深刻。

先說結論:

  • 并不是所有的異常都會觸發事務回滾,所以最好指定rollbackFor(一般圖省事都直接指定Exception.class)
  • propagation是寫給調用者看的,而不是寫給被調用者看的(一句話解釋有點晦澀,后面展開)

最好指定rollbackFor

我們來看看rollbackFor的注釋:

也即是說,雖然rollbackFor默認指定了異常類型,但僅僅包括Error和RuntimeException。如果是其他自定義的業務異常,就不會觸發回滾(理論上是這樣,但通常業務異常都會繼承自RuntimeException,因為運行時異常無需強制處理)。

propagation的案例

接下來結合上面的selectUser(),我們來看看propagation每種情況的具體演示。

Propagation.REQUIRED

如果當前存在事務,則加入該事務,如果當前不存在事務,則創建一個新的事務。( 也就是說如果A方法和B方法都添加了注解,在默認傳播模式下,A方法內部調用B方法,會把兩個方法的事務合并為一個事務

selectUser()和updateUser()都加上事務控制時,雖然內部調用還是this.updateUser(),是普通方法調用,但整體上在selectUser()的事務中。

Propagation.SUPPORTS

如果當前存在事務,則加入該事務;如果當前不存在事務,則以非事務的方式繼續運行。

事務失效了。

原因是test方法調用userService.selectUser()時,本身是沒有事務的,而剛好selectUser()使用了SUPPORT:當前存在事務,則加入事務;如果不存在事務,則以非事務方式繼續運行。

這里所謂的當前,其實就是指調用方,即調用selectUser()的方法是否存在事務。由于test不存在事務,于是selectUser()也就沒有事務,而this.updateUser()本身事務失效,所以最終整個調用事務失效。

如果希望selectUser()事務起效,SUPPORTS的情況下,可以給調用方加@Transactional:

Propagation.MANDATORY

mandatory:強制的。

如果當前存在事務,則加入該事務;如果當前不存在事務,則拋出異常。也就是要求調用方必須存在事務。

同理,給test方法加上事務,那么selectUser()就會處于test的事務中,不會拋異常。

看到這里,大家是不是同意本小節開頭說的那句話了呢:

propagation是寫給調用者(test)看的,而不是寫給被調用者(updateUser)看的

Propagation.REQUIRES_NEW

重新創建一個新的事務,和外面的事務相互獨立。

比如:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
methodA(){// 1.插入a表...// 2.調用methodBmethodB();// 3.在methodA拋異常,回滾int i = 1/0;
}@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
methodB(){// 4.插入b表
}

methodA拋異常了,回滾了,但是methodB還是會插入記錄。因為methodB是REQUIRES_NEW,自己起了一個事務。也就是說,methodA和methodB各管各的,無論是誰的內部拋異常都不會影響外部回滾。

Propagation.NOT_SUPPORTED

以非事務的方式運行,無論調用者是否存在事務,自己都不受其影響。和Propagation.REQUIRES_NEW有點像,但NOT_SUPPORTED自己是沒有事務的。

Propagation.NEVER

以非事務的方式運行,如果當前存在事務,則拋出異常。即如果methodB設置了NEVER,而methodA設置了事務,那么調用methodB時就會拋異常。它不想在有事務的方法內運行。

Propagation.NESTED

和Propagation.REQUIRED效果一樣。

最后說一句,我平時就看過第一、第二種。99%情況下都是默認REQUIRED,只需注意rollbackFor即可。

本文討論是同類內的非事務方法調用事務方法,而不是調用其他類的事務方法,那和代理對象調用沒區別。

@Service
class UserServiceImpl implements UserService {@Autowiredprivate StudentService studentService;public void methodA(){// 方法內部的一些操作...// 調用同類的methodB()methodB();// 調用StudentService的方法studentService.methodC();     }@Transactional(rollbackFor = Exception.class)public void methodB(){}
}

另外,大家以前可能在各種平臺看過@Async注解也存在同類方法調用失效的問題。看完這篇文章,你覺得是為什么呢~

作者簡介:大家好,我是smart哥,前中興通訊、美團架構師,現某互聯網公司CTO

進群,大家一起學習,一起進步,一起對抗互聯網寒冬

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/215673.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/215673.shtml
英文地址,請注明出處:http://en.pswp.cn/news/215673.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

解讀 | 為什么有很多名人讓人們警惕人工智能

大家好&#xff0c;我是極智視界&#xff0c;歡迎關注我的公眾號&#xff0c;獲取我的更多前沿科技分享 邀您加入我的知識星球「極智視界」&#xff0c;星球內有超多好玩的項目實戰源碼和資源下載&#xff0c;鏈接&#xff1a;https://t.zsxq.com/0aiNxERDq 這個話題總能引起很…

六、ZGC深度剖析

一、引言 對于Java 程序員來說&#xff0c;JVM 幫助我們做了很多事情。 JVM是虛擬機&#xff0c;能夠識別字節碼&#xff0c;就是class文件或者你打包的jar文件&#xff0c;運行在操作系統上。 JVM幫我們實現了跨平臺&#xff0c;你只需要編譯一次&#xff0c;就可以在不同的…

在線課堂知識付費小程序源碼系統 開發組合PHP+MySQL:用手機隨時隨地地學習,講師親自在線授業解惑 帶安裝部署教程

近年來&#xff0c;人們對于學習的需求也日益增加。傳統的課堂教學已經無法滿足人們的學習需求&#xff0c;而在線課堂則能夠讓人們隨時隨地地進行學習。同時&#xff0c;隨著知識付費的興起&#xff0c;越來越多的講師也愿意將自己的知識和經驗分享給更多的人。因此&#xff0…

Mysql社區版日志審計插件

過去從Mysql官方自帶general.log日志的相比其他插件性能是最差的&#xff0c;我們考慮參考行業中較好的插件是MariaDB Audit Plugin, 可惜并不兼容mysql 5.7與mysql 8.0以上版本。 采用github開源項目&#xff0c;該項目支持MySQL 5.7和MySQL 8.0兩個分支。 https://github.c…

如何管理醫療設備用電?這才是最佳方法!

隨著社會對可持續發展和環保的關注不斷上升&#xff0c;蓄電池監控系統作為能源存儲和管理的關鍵技術&#xff0c;正在嶄露頭角。 蓄電池監控系統不僅為能源行業帶來了新的可能性&#xff0c;同時也為各個領域的能源使用者提供了更加智能、高效的解決方案。 客戶案例 工業生產…

ansible部署安裝Tomcat

我們需要用到的文件jdk以及tomcat安裝包 下載鏈接:https://pan.baidu.com/s/1sjG8Yl8k-SUbOv7KwKXZMA 提取碼&#xff1a;t71z 準備n臺機器&#xff08;我這里就簡單部署三臺機器&#xff09; ansible的安裝部署以及配置可以看博主之前的文章自動化運維工具-ansible部署 ansib…

建筑可視化數據大屏匯總,UI源文件(PC端大屏設計)

酷炫的大屏設計讓數據更好的展現&#xff0c;方便業務人員分析數據&#xff0c;輔助領導決策。現在分享大屏Photoshop源文件&#xff0c;以下為部分截圖示意。 劃重點&#xff1a;文末可獲得完整素材包~ 01 科技建筑平臺數據可視化 02 建筑公司可視化數據匯總平臺 03 深藍…

計算機視覺-機器學習-人工智能 頂會會議召開地址

計算機視覺-機器學習-人工智能 頂會會議召開地址 最近應該要整理中文資料的參考文獻&#xff0c;很多會議文獻都需要補全會議地點&#xff08;新國標要求&#xff09;。四處百度感覺也挺麻煩的&#xff0c;而且沒有比較齊全的網站可以搜索。因此自己整理了一下計算機視覺-機器…

JVM虛擬機系統性學習-對象存活判斷算法、對象引用類型和垃圾清除算法

垃圾回收 在 JVM 中需要對沒有被引用的對象&#xff0c;也就是垃圾對象進行垃圾回收 對象存活判斷算法 判斷對象存活有兩種方式&#xff1a;引用計數法、可達性分析算法 引用計數法 引用計數法通過記錄每個對象被引用的次數&#xff0c;例如對象 A 被引用 1 次&#xff0c…

c#面試基礎語法——現有?個整數number,請寫?個?法判斷這個整數是否是2的N次?

1.number%20 取余&#xff08;取模&#xff09;只能判斷number是不是2的倍數但不一定是2的N次方&#xff0c;如&#xff1a;6%20但是他并不是2的N次方 2.(number&(number-1))0 原理&#xff1a;如果number是2的N次方則表示2進制位只有一位是1。如&#xff1a;2 &#xff08…

多示例VS多標簽VS多示例多標簽-week2

一、多示例 多示例學習屬于弱監督學習中的一種&#xff0c;在對模型進行訓練時&#xff0c;我們需要把訓練數據分成正負包&#xff0c;再將每個包分成大小相同的示例&#xff0c;并且我們只對包的正負進行標注&#xff0c;而不對示例進行分類。當某個包被標識為正時&#xff0c…

Java怎么實現動態代理?

Java怎么實現動態代理&#xff1f; Java中實現動態代理主要依賴于java.lang.reflect.Proxy類和java.lang.reflect.InvocationHandler接口。動態代理可以用于在運行時創建代理類及其實例。以下是一個簡單的動態代理示例&#xff1a; 首先&#xff0c;定義一個接口&#xff1a;…

Python常見面試知識總結(二):數據結構、類方法及異常處理

【十三】Python中assert的作用&#xff1f; Python中assert&#xff08;斷言&#xff09;用于判斷一個表達式&#xff0c;在表達式條件為 f a l s e false false的時候觸發異常。 斷言可以在條件不滿足程序運行的情況下直接返回錯誤&#xff0c;而不必等待程序運行后出現崩潰…

光伏設計方案:實現清潔能源的未來

隨著全球氣候變化和能源需求日益增長的問題日益嚴重&#xff0c;光伏發電作為一種清潔、可再生的能源形式&#xff0c;正逐漸成為全球能源轉型的主力軍。而在光伏發電技術的廣泛應用中&#xff0c;一個優秀的光伏設計方案對于實現高效、穩定和安全的發電目標至關重要。 光伏設…

【項目管理】如何用思維導圖做計劃?

思維導圖是一種可視化的思維工具&#xff0c;它可以讓我們的思考過程變得很直觀。它可以幫助我們考慮到計劃的各個方方面面&#xff0c;確定各要素之間的關系。 思維導圖總結功能很強&#xff0c;完成計劃后&#xff0c;可以用思維導圖進行總結&#xff0c;為下一次做計劃積累…

Linux中tar命令詳解

具體用法 tar命令是Linux中用于打包和壓縮文件或目錄的命令&#xff0c;常用于備份和歸檔。它可以將多個文件或目錄打包成一個單一的文件&#xff0c;并可以選擇是否壓縮打包文件。 打包文件或目錄 tar -cvf archive.tar file1 file2 directory1上面的命令將file1、file2和dir…

使用【ShardingSphere】分庫分表

前言 ShardingSphere可以支撐分庫分表&#xff0c;剛果商城采用了垂直分庫&#xff08;根據不同業務拆分數據庫&#xff09;&#xff0c;因此此文章只演示水平分表。 垂直分庫 不同業務拆分為不同的數據庫&#xff08;例如商城業務&#xff09; 水平分表 分表可以通過將大表拆…

2024年軟考高項還是機考嗎?附常見問題答疑

2024年軟考高項實行機考&#xff08;三科均為機考&#xff0c;綜合知識考試時間為上午8:30-11:00&#xff0c;案例論文聯考&#xff0c;考試時間為下午14:30-18:00&#xff09;&#xff0c;本文為大家整理了一些機考常見問題&#xff0c;希望對大家有所幫助。 一、軟考高項機考…

React Hooks學習指北

一、前言 在當今的前端開發環境中&#xff0c;越來越多的開發者認可了 Hooks 的強大能力&#xff0c;并紛紛加入到 Hooks 的使用大軍中&#xff1a; 2019 年 2 月&#xff0c;React 正式發布 v16.8 版本&#xff0c;引入 Hooks 能力&#xff08;最新的 v18 中&#xff0c;還新…

移液器吸頭材質選擇——PFA吸頭在半導體化工行業的應用

PFA吸頭是一種高性能移液器配件&#xff0c;這種材料具有優異的耐化學品、耐熱和電絕緣性能&#xff0c;使得PFA吸頭在應用中表現出色。那么它有哪些特點呢&#xff1f; 首先&#xff0c;PFA吸頭具有卓越的耐化學腐蝕性能。無論是酸性溶液、堿性溶液還是有機溶劑&#xff0c;P…