作者簡介:大家好,我是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
進群,大家一起學習,一起進步,一起對抗互聯網寒冬