Spring事務@Transactional注解四種不生效案例場景分析
背景
在我們工作中,經(jīng)常會用到 @Transactional 聲明事務,不正確的使用姿勢會導致注解失效,下面就來分析四種最常見的@Transactional事務不生效的 Case:
類內(nèi)部訪問:A 類的 a1 方法沒有標注 @Transactional,a2 方法標注 @Transactional,在 a1 里面調(diào)用 a2;
私有方法:將 @Transactional 注解標注在非 public 方法上;
異常不匹配:@Transactional 未設置 rollbackFor 屬性,方法返回 Exception 等異常;
多線程:主線程和子線程的調(diào)用,線程拋出異常。
示例代碼
UserDao 接口,操作數(shù)據(jù)庫;UserController 實現(xiàn)業(yè)務邏輯,聲明事務,調(diào)用 UserController.testSuccess 方法,事務聲明生效
// 提供的接口 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); }
@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("事務生效"); } }
為了快速說明問題,直接在controller中實現(xiàn)了業(yè)務邏輯和事務聲明,不代表生產(chǎn)環(huán)境中的代碼分層
1. 類內(nèi)部訪問
在類 UserController 中新增一個方法 testInteralCall():
public void testInteralCall() throws Exception { testSuccess(); throw new Exception("事務不生效:類內(nèi)部訪問"); }
這里 testInteralCall() 沒有標注 @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=女)
從上面的輸出可以看到,事務并沒有回滾,這個是什么原因呢?
因為 @Transactional 的工作機制是基于 AOP 實現(xiàn),AOP 是使用動態(tài)代理實現(xiàn)的,如果通過代理直接調(diào)用 testSuccess(),通過 AOP 會前后進行增強,增強的邏輯其實就是在 testSuccess() 的前后分別加上開啟、提交事務的邏輯。
現(xiàn)在是通過 testInteralCall() 去調(diào)用 testSuccess(),testSuccess() 前后不會進行任何增強操作,也就是類內(nèi)部調(diào)用,不會通過代理方式訪問。
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("測試事務生效"); }
直接使用時,下面這種場景不太容易出現(xiàn),因為 IDEA 會有提醒,文案為: Methods annotated with '@Transactional' must be overridable,至于深層次的原理,源碼部分會給你解讀。
3. 異常不匹配
這里的 @Transactional 沒有設置 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("事務不生效:異常不匹配"); }
@Transactional 注解默認處理運行時異常,即只有拋出運行時異常時,才會觸發(fā)事務回滾,否則并不會回滾,至于深層次的原理,源碼部分會給你解讀。
4. 多線程
父線程拋出異常
父線程拋出異常,子線程不拋出異常:
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("測試事務不生效"); }
父線程拋出線程,事務回滾,因為子線程是獨立存在,和父線程不在同一個事務中,所以子線程的修改并不會被回滾
子線程拋出異常
父線程不拋出異常,子線程拋出異常:
public void testSuccess() throws Exception { Integer id = 1; MyUser user = query(id); System.out.println("原記錄:" + user); update(id); throw new Exception("測試事務不生效"); } @Transactional(rollbackFor = Exception.class) public void testMultThread() throws Exception { new Thread(new Runnable() { @SneakyThrows @Override public void run() { testSuccess(); } }).start(); }
由于子線程的異常不會被外部的線程捕獲,所以父線程不拋異常,事務回滾沒有生效。
源碼解讀
@Transactional 執(zhí)行機制
我們只看最核心的邏輯,代碼中的 interceptorOrInterceptionAdvice 就是 TransactionInterceptor 的實例,入?yún)⑹?this 對象。
紅色方框有一段注釋,大致翻譯為 “它是一個攔截器,所以我們只需調(diào)用即可:在構(gòu)造此對象之前,將靜態(tài)地計算切入點。”
this 是 ReflectiveMethodInvocation 對象,成員對象包含 UserController 類、testSuccess() 方法、入?yún)⒑痛韺ο蟮取?/p>
進入 invoke() 方法后:
前方高能?。。∵@里就是事務的核心邏輯,包括判斷事務是否開啟、目標方法執(zhí)行、事務回滾、事務提交。
private 導致事務不生效原因
在上面這幅圖中,第一個紅框區(qū)域調(diào)用了方法 getTransactionAttribute(),主要是為了獲取 txAttr 變量,它是用于讀取 @Transactional 的配置,如果這個 txAttr = null,后面就不會走事務邏輯,我們看一下這個變量的含義:
我們直接進入 getTransactionAttribute(),重點關注獲取事務配置的方法。
前方高能!??!這里就是 private 導致事務不生效的原因所在,allowPublicMethodsOnly() 一直返回 false,所以重點只關注 isPublic() 方法。
異常不匹配原因
我們繼續(xù)回到事務的核心邏輯,因為主方法拋出 Exception() 異常,進入事務回滾的邏輯:
進入 rollbackOn() 方法,判斷該異常是否能進行回滾,這個需要判斷主方法拋出的 Exception() 異常,是否在 @Transactional 的配置中:
我們進入 getDepth() 看一下異常規(guī)則匹配邏輯,因為我們對 @Transactional 配置了 rollbackFor = Exception.class,所以能匹配成功:
示例中的 winner 不為 null,所以會跳過下面的環(huán)節(jié)。但是當 winner = null 時,也就是沒有設置 rollbackFor 屬性時,會走默認的異常捕獲方式。
前方高能?。?!這里就是異常不匹配原因的原因所在,我們看一下默認的異常捕獲方式:
是不是豁然開朗,當沒有設置 rollbackFor 屬性時,默認只對 RuntimeException 和 Error 的異常執(zhí)行回滾。
以上就是Spring事務@Transactional注解的四種不生效案例場景分析的詳細內(nèi)容,更多關于Spring事務@Transactional不生效的資料請關注腳本之家其它相關文章!
相關文章
Mybatis?saveAndUpdate空值不更新問題及解決
這篇文章主要介紹了Mybatis?saveAndUpdate空值不更新問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02SpringBoot如何實現(xiàn)同域SSO(單點登錄)
單點登錄(SingleSignOn,SSO),就是通過用戶的一次性鑒別登錄。即在多個應用系統(tǒng)中,只需要登錄一次,就可以訪問其他相互信任的應用系統(tǒng),本文將介紹SpringBoot如何實現(xiàn)同域SSO(單點登錄)2021-05-05SpringCloud Gateway HttpWebHandlerAdapter鏈路調(diào)用請求流程介
Spring Cloud Gateway旨在為微服務架構(gòu)提供一種簡單有效的、統(tǒng)一的 API 路由管理方式。Spring Cloud Gateway 作為 Spring Cloud 生態(tài)系中的網(wǎng)關,它不僅提供統(tǒng)一的路由方式,并且基于 Filter 鏈的方式提供了網(wǎng)關基本的功能,例如:安全、監(jiān)控/埋點和限流等2022-10-10Springboot Apollo配置yml的問題及解決方案
這篇文章主要介紹了Springboot Apollo配置yml的問題及解決方案,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-06-06