事務沒回滾?來,我們從現象到原理一起分析一波!
- 2019 年 10 月 4 日
- 筆記
@Transactional-曾經年少只懂用
當我還是一個初出校園的小程式猿的時候,碰到過這樣的一個問題:
需求是有兩個表,分別是訂單表和訂單附加資訊表,其中訂單表一定要落數據,訂單附加資訊表保存的時候如果有異常,則回滾,但是不能影響訂單表的數據。
美麗的測試小姐姐測試時發現訂單表沒有數據,於是對我說:大兄弟,瞅一眼,啥情況啊。
我檢查程式碼的時候發現保存訂單表和訂單附加資訊表的方法上都加了@Transactional註解,當保存訂單的方法順利執行完成,如果保存訂單附加資訊的方法拋出異常也會被捕獲。最後得出結論:沒毛病呀,很正常呀。
這個時候我完美的體現作為一個程式猿應該的氣質,質疑測試的使用環境,方式,甚至執行姿勢,站起來對著小姐姐喊到:"再來一筆試一試,我盯著日誌!"但是,看到結果的那一刻,我小臉一紅,默默的坐了下來,安心的排查起問題來。
場景復現,程式碼展示:
1.可以Http介面調用,也可以是Dubbo調用。提供一個入口即可。這裡為了方便演示,用Http介面調用。程式碼如下:

2.前面的Controller中注入的就是下面的OrderBizService。這個OrderBizService中注入了保存訂單表的IOrderInfoService和保存訂單附加資訊表的IOrderInfoExtService。程式碼如下:

3.OrderBizService中有三個關鍵的點已經在圖片中進行了說明。
4.項目整體結構如下:

5.訂單表和訂單附加資訊表結構如下:

通過以上操作,我們進行了場景鋪墊、程式碼復現、表結構展示。我們先列出我們預期的結果:
order_info表中有一條數據,order_info_ext表中沒有數據。
那麼接下來,我們執行一下看看結果吧。在本地啟動成功後,調用鏈接:http://127.0.0.1:8080/transaction

6.看到執行結果的時候,氣氛突然變的尷尬了起來:OMG,這和我的預期不一樣啊!到底為啥呢?我用了@Transactional註解了呀。怪只怪自己
曾經年少只懂用,頭髮全黑且茂密。
看到這裡了,大家可以先回想一下自己在項目中寫的有關於事務的程式碼,是不是也是這樣操作的: 1.通過Controller調用某個Service的方法,假如是saveMian(這個方法有沒有@Transactional註解都可以) 2.Service的saveMian方法裡面直接調用了這個Service裡面某個有@Transactional註解的方法,假如是saveSub(直接調用:saveSub或者this.saveSub) 3.如果你的回答是有這樣的程式碼的話,那麼大兄弟或者小姐姐,我告訴你:如果這個程式碼別人寫的,你踩坑了;如果這個程式碼是你寫的,你留坑了。
如果你去問前輩,那麼這個時候前輩就會告訴你:
聽哥一句勸:在bean中不要直接調用或者使用this調用,某個被@Transactional註解標註的方法。this下@Transactional註解是不生效的。
@Transactional-現在長大知原理
前面我們已經看到了現象,接下來我們需要深入到原理的部分,一起透過現象看本質了。

可以看出,這個this並不是動態代理的子類對象,而是一個原始對象,所以this調用的orderExt()方法雖然加了@Transactional註解,但是無法卻通過動態代理來增強,從而導致事務失效。
那麼上面的程式碼其實就可以簡化成下面的樣子,這樣就一目了然了:

相信通過上面的程式碼,大家就知道為啥兩個表都能插入成功了。想不到吧,這令人窒息的操作
那麼問題來了:是什麼原因導致執行結果和我們的預期結果不一致的呢?這個問題我先按下不表。先說說結論,大家帶著結論往下看會容易理解一點。
結論就是:Spring的事務是基於AOP實現的,AOP是基於動態代理實現的.所以@Transactional註解如果想要生效,那麼其調用方,需要是被Spring動態代理後的類。
在這插一句:當年在大學裡面學到AOP和IOC的時候,感覺這兩個概念難以理解的同時又很牛逼。但是當我在工作中實踐,慢慢理解的時候,我才發現:面向切面編程(AOP)、控制反轉(IOC)這兩個翻譯是多麼的精準的描述啊!
現在有了結論,我們知道了關鍵是調用方是需要被動態代理的類。
那麼解決的方法就呼之欲出了,核心點就是如何獲得OrderBizService的動態代理對象。那麼這裡我給出三種解決方案
方案一.自己注入自己
我們把程式碼改成下面的樣子,並打上斷點執行起來:

通過調試可以看到,orderExt()方法的調用方,orderBizService已經是被CGLIB動態代理增強後的類了。所以其事務能生效。
方案二.注入ApplicationContext

也是只需要兩處改動:
- 注入ApplicationContext,spring上下文。
- 從ApplicationContext中獲取到OrderBizService。
方案三.藉助AopContext
1.首先第一步,需要進行代理暴露。expose-proxy="true"

Aop的代理暴露默認是false。如果我們需要取出當前的代理,那麼需要是設置為true。這樣,代理類就會被設置到AopContxt中。
在這裡,我可以說一下之前說這個aop.xml我覺得很重要的原因。 在spring時代,我們需要對xml進行各種配置。其中一個就是<aop:aspectj-autoproxy/> ,對我而言,我當時也是僅僅知道要加這行程式碼,不知道為什麼要加。隨著知識框架的逐漸完善,再回頭研究它,發現很快就能理解。假設當初我剛學這一塊知識,知識還沒成體系,我硬要深入到源碼,最後得到的結果也一定是在裡面花費了大量時間,得到一個模稜兩可的感覺,打擊了自己的自信心。 我想表達的意思就是:對於初學者而言,有的時候不必花太多的時候去追求細枝末節,時間會幫你搭建知識體系,有了體系,你回過頭看,發現有的問題,迎刃而解。所以,如果作為初學者,你只管保持大方向的學習,保持努力,保持進步,剩下細枝末節的東西,時間會幫你解決。
第2步:引入aop.xml文件:

其實這裡用@EnableAspectJAutoProxy(exposeProxy =true)註解也可以,就可以不需要aop.xml文件,而且更加符合springboot的思想。
第3步:從AopContext中取出OrderBizService.

對於AopContext多說兩句:
第一句:AopContext裡面維護了一個ThreadLocal:
private static final ThreadLocal currentProxy =new NamedThreadLocal<>("Current AOP proxy");

第二句:因為AopContext默認是不暴露當前代理類的,所以要@EnableAspectJAutoProxy(exposeProxy =true)或者<aop:aspectj-autoproxy expose-proxy="true"/>:

三種解決方案分析完了,接下來,我想我們一起看看日誌:
orderExt()方法事務不生效時主要日誌:

從日誌我們可以看出從頭到尾只有一個事務。根據我們上面的分析,我想你應該清楚為什麼了。
orderExt()方法事務生效時主要日誌:

通過日誌可以很直觀的分析出:事務生效了。所以有的時候,靜下心來分析日誌也是很有必要的。
最後吟詩一首: