公司新來一個(gè)同事,把 @Transactional 注解運(yùn)用得爐火純青...

作者:沉默王二
Java 程序員進(jìn)階之路:https://tobebetterjavaer.com
Java 后端面試的時(shí)候,面試官經(jīng)常會問到 @Transactional 的原理,以及容易踩的坑,之前一面百度,就遇到過,今天就帶大家把這幾塊知識吃透。歡迎大家關(guān)注二哥的三劍客團(tuán)隊(duì)之一樓仔。
這篇文章,會先講述 @Transactional 的 4 種不生效的 Case,然后再通過源碼解讀,分析 @Transactional 的執(zhí)行原理,以及部分 Case 不生效的真正原因。

項(xiàng)目準(zhǔn)備
下面是 DB 數(shù)據(jù)和 DB 操作接口:
| uid | uname | usex |
|---|---|---|
| 1 | 張三 | 女 |
| 2 | 陳恒 | 男 |
| 3 | 樓仔 | 男 |
// 提供的接口
public interface UserDao {
// select * from user_test where uid = "#{uid}"
public MyUser selectUserById(Integer uid);
// update user_test set uname =#{uname},usex = #{usex} where uid = #{uid}
public int updateUser(MyUser user);
}
基礎(chǔ)測試代碼,testSuccess() 是事務(wù)生效的情況:
@Service
public class UserController {
@Autowired
private UserDao userDao;
public void update(Integer id) {
MyUser user = new MyUser();
user.setUid(id);
user.setUname("張三-testing");
user.setUsex("女");
userDao.updateUser(user);
}
public MyUser query(Integer id) {
MyUser user = userDao.selectUserById(id);
return user;
}
// 正常情況
@Transactional(rollbackFor = Exception.class)
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原記錄:" + user);
update(id);
throw new Exception("事務(wù)生效");
}
}
事務(wù)不生效的幾種 Case
主要講解 4 種事務(wù)不生效的 Case:
類內(nèi)部訪問:A 類的 a1 方法沒有標(biāo)注 @Transactional,a2 方法標(biāo)注 @Transactional,在 a1 里面調(diào)用 a2; 私有方法:將 @Transactional 注解標(biāo)注在非 public 方法上; 異常不匹配:@Transactional 未設(shè)置 rollbackFor 屬性,方法返回 Exception 等異常; 多線程:主線程和子線程的調(diào)用,線程拋出異常。
Case 1: 類內(nèi)部訪問
我們在類 UserController 中新增一個(gè)方法 testInteralCall():
public void testInteralCall() throws Exception {
testSuccess();
throw new Exception("事務(wù)不生效:類內(nèi)部訪問");
}
這里 testInteralCall() 沒有標(biāo)注 @Transactional,我們再看一下測試用例:
public static void main(String[] args) throws Exception {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserController uc = (UserController) applicationContext.getBean("userController");
try {
uc.testSuccess();
} finally {
MyUser user = uc.query(1);
System.out.println("修改后的記錄:" + user);
}
}
// 輸出:
// 原記錄:MyUser(uid=1, uname=張三, usex=女)
// 修改后的記錄:MyUser(uid=1, uname=張三-testing, usex=女)
從上面的輸出可以看到,事務(wù)并沒有回滾,這個(gè)是什么原因呢?
因?yàn)?@Transactional 的工作機(jī)制是基于 AOP 實(shí)現(xiàn),AOP 是使用動(dòng)態(tài)代理實(shí)現(xiàn)的,如果通過代理直接調(diào)用 testSuccess(),通過 AOP 會前后進(jìn)行增強(qiáng),增強(qiáng)的邏輯其實(shí)就是在 testSuccess() 的前后分別加上開啟、提交事務(wù)的邏輯,后面的源碼會進(jìn)行剖析。
現(xiàn)在是通過 testInteralCall() 去調(diào)用 testSuccess(),testSuccess() 前后不會進(jìn)行任何增強(qiáng)操作,也就是類內(nèi)部調(diào)用,不會通過代理方式訪問。
如果還是不太清楚,推薦再看看這篇文章,里面有完整示例,非常完美詮釋“類內(nèi)部訪問”不能前后增強(qiáng)的原因:https://blog.csdn.net/Ahuuua/article/details/123877835
Case 2: 私有方法
在私有方法上,添加 @Transactional 注解也不會生效:
@Transactional(rollbackFor = Exception.class)
private void testPirvateMethod() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原記錄:" + user);
update(id);
throw new Exception("測試事務(wù)生效");
}
直接使用時(shí),下面這種場景不太容易出現(xiàn),因?yàn)?IDEA 會有提醒,文案為: Methods annotated with '@Transactional' must be overridable,至于深層次的原理,源碼部分會給你解讀。
Case 3: 異常不匹配
這里的 @Transactional 沒有設(shè)置 rollbackFor = Exception.class 屬性:
@Transactional
public void testExceptionNotMatch() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原記錄:" + user);
update(id);
throw new Exception("事務(wù)不生效:異常不匹配");
}
測試方法:同 Case1
// 輸出:
// 原記錄:User[uid=1,uname=張三,usex=女]
// 修改后的記錄:User[uid=1,uname=張三-test,usex=女]
@Transactional 注解默認(rèn)處理運(yùn)行時(shí)異常,即只有拋出運(yùn)行時(shí)異常時(shí),才會觸發(fā)事務(wù)回滾,否則并不會回滾,至于深層次的原理,源碼部分會給你解讀。
Case 4: 多線程
下面給出兩個(gè)不同的姿勢,一個(gè)是子線程拋異常,主線程 ok;一個(gè)是子線程 ok,主線程拋異常。
父線程拋出異常
父線程拋出異常,子線程不拋出異常:
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原記錄:" + user);
update(id);
}
@Transactional(rollbackFor = Exception.class)
public void testMultThread() throws Exception {
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
testSuccess();
}
}).start();
throw new Exception("測試事務(wù)不生效");
}
父線程拋出線程,事務(wù)回滾,因?yàn)樽泳€程是獨(dú)立存在,和父線程不在同一個(gè)事務(wù)中,所以子線程的修改并不會被回滾,
子線程拋出異常
父線程不拋出異常,子線程拋出異常:
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原記錄:" + user);
update(id);
throw new Exception("測試事務(wù)不生效");
}
@Transactional(rollbackFor = Exception.class)
public void testMultThread() throws Exception {
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
testSuccess();
}
}).start();
}
由于子線程的異常不會被外部的線程捕獲,所以父線程不拋異常,事務(wù)回滾沒有生效。
源碼解讀
下面我們從源碼的角度,對 @Transactional 的執(zhí)行機(jī)制和事務(wù)不生效的原因進(jìn)行解讀。
@Transactional 執(zhí)行機(jī)制
我們只看最核心的邏輯,代碼中的 interceptorOrInterceptionAdvice 就是 TransactionInterceptor 的實(shí)例,入?yún)⑹?this 對象。
紅色方框有一段注釋,大致翻譯為 “它是一個(gè)攔截器,所以我們只需調(diào)用即可:在構(gòu)造此對象之前,將靜態(tài)地計(jì)算切入點(diǎn)。”

this 是 ReflectiveMethodInvocation 對象,成員對象包含 UserController 類、testSuccess() 方法、入?yún)⒑痛韺ο蟮取?/p>
進(jìn)入 invoke() 方法后:

前方高能!!!這里就是事務(wù)的核心邏輯,包括判斷事務(wù)是否開啟、目標(biāo)方法執(zhí)行、事務(wù)回滾、事務(wù)提交。

private 導(dǎo)致事務(wù)不生效原因
在上面這幅圖中,第一個(gè)紅框區(qū)域調(diào)用了方法 getTransactionAttribute(),主要是為了獲取 txAttr 變量,它是用于讀取 @Transactional 的配置,如果這個(gè) txAttr = null,后面就不會走事務(wù)邏輯,我們看一下這個(gè)變量的含義:

我們直接進(jìn)入 getTransactionAttribute(),重點(diǎn)關(guān)注獲取事務(wù)配置的方法。

前方高能!!!這里就是 private 導(dǎo)致事務(wù)不生效的原因所在,allowPublicMethodsOnly() 一直返回 false,所以重點(diǎn)只關(guān)注 isPublic() 方法。

下面通過位與計(jì)算,判斷是否為 Public,對應(yīng)的幾類修飾符如下:
PUBLIC: 1 PRIVATE: 2 PROTECTED: 4

看到這里,是不是豁然開朗了,有沒有覺得很有意思呢~~
異常不匹配原因
我們繼續(xù)回到事務(wù)的核心邏輯,因?yàn)橹鞣椒⊕伋?Exception() 異常,進(jìn)入事務(wù)回滾的邏輯:

進(jìn)入 rollbackOn() 方法,判斷該異常是否能進(jìn)行回滾,這個(gè)需要判斷主方法拋出的 Exception() 異常,是否在 @Transactional 的配置中:

我們進(jìn)入 getDepth() 看一下異常規(guī)則匹配邏輯,因?yàn)槲覀儗?@Transactional 配置了 rollbackFor = Exception.class,所以能匹配成功:

示例中的 winner 不為 null,所以會跳過下面的環(huán)節(jié)。但是當(dāng) winner = null 時(shí),也就是沒有設(shè)置 rollbackFor 屬性時(shí),會走默認(rèn)的異常捕獲方式。

前方高能!!!這里就是異常不匹配原因的原因所在,我們看一下默認(rèn)的異常捕獲方式:

是不是豁然開朗,當(dāng)沒有設(shè)置 rollbackFor 屬性時(shí),默認(rèn)只對 RuntimeException 和 Error 的異常執(zhí)行回滾。
一個(gè)人可以走得很快,但一群人才能走得更遠(yuǎn)。歡迎加入二哥的編程星球,里面的每個(gè)球友都非常的友善,除了鼓勵(lì)你,還會給你提出合理的建議。星球提供的三份專屬專欄《Java 面試指南》、《編程喵 ??(Spring Boot+Vue 前后端分離)實(shí)戰(zhàn)項(xiàng)目筆記》、《Java 版 LeetCode 刷題筆記》,干貨滿滿,價(jià)值連城。



已經(jīng)有 670 多名 小伙伴加入二哥的編程星球了,如果你也需要一個(gè)良好的學(xué)習(xí)氛圍,戳鏈接加入我們的大家庭吧!這是一個(gè) Java 學(xué)習(xí)指南 + 編程實(shí)戰(zhàn) + LeetCode 刷題的私密圈子,你可以向二哥提問、幫你制定學(xué)習(xí)計(jì)劃、跟著二哥一起做實(shí)戰(zhàn)項(xiàng)目,沖沖沖。


沒有什么使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧靜的港灣,我是不系之舟。
推薦閱讀:
今年這情況,真有點(diǎn)想讀研了 專升本上岸的秘訣 憤怒,一個(gè)破培訓(xùn)班要價(jià) 28 萬 公司不卡學(xué)歷,卻擔(dān)心自己實(shí)力不夠 今年面試有點(diǎn)小難,還是要沖 人生當(dāng)中掙到的第一個(gè) 1 萬元 新一代開源免費(fèi)的終端工具,太酷了 Java 后端四件套學(xué)習(xí)資料

