Spring事務,非事務方法調用事務方法,事務不生效


最近在做項目中有個業務是每天定時更新xx的數據,某條記錄更新中數據出錯,不影響整體數據,只需記錄下來並回滾當條記錄所關聯的表數據; 好啊,這個簡單,接到任務后,樓主我三下五除二就寫完了,由於這個業務還是有些麻煩,我就在一個service里拆成了兩個方法去執行,一個方法(A)是查詢數據與驗證組裝數據,另外一個方法(B)更新這條數據所對應的表(執行的時候是方法A中調用方法B);由於這個數據是循環更新,所以我想的是,一條數據更新失敗直接回滾此條數據就是,不會影響其他數據,其他的照常更新,所以我就在方法B上加了事務,方法A沒有加; 以為很完美,自測一下正常,ok通過,再測試一下報錯情況,是否回滾,一測,沒回滾,懵圈兒?.以為代碼寫錯了,改了幾處地方,再測了幾次,均沒回滾.這下是真難受了.

好啦,寫到這里,相信各位看官心里肯定在嘲諷老弟了,spring的傳播機制都沒搞明白(/難受); 

下面開始一步步分析解決問題:

首先我們來看下spring事務的傳播機制及原因分析;

PROPAGATION_REQUIRED -- 支持當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇。
PROPAGATION_SUPPORTS -- 支持當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY -- 支持當前事務,如果當前沒有事務,就拋出異常。
PROPAGATION_REQUIRES_NEW -- 新建事務,如果當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED -- 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER -- 以非事務方式執行,如果當前存在事務,則拋出異常。
PROPAGATION_NESTED -- 如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則進行與PROPAGATION_REQUIRED類似的操作。 
spring默認的是PROPAGATION_REQUIRED機制,如果方法A標注了注解@Transactional 是完全沒問題的,執行的時候傳播給方法B,因為方法A開啟了事務,線程內的connection的屬性autoCommit=false,並且執行到方法B時,事務傳播依然是生效的,得到的還是方法A的connection,autoCommit還是為false,所以事務生效;反之,如果方法A沒有注解@Transactional 時,是不受事務管理的,autoCommit=true,那么傳播給方法B的也為true,執行完自動提交,即使B標注了@Transactional ;

在一個Service內部,事務方法之間的嵌套調用,普通方法和事務方法之間的嵌套調用,都不會開啟新的事務.是因為spring采用動態代理機制來實現事務控制,而動態代理最終都是要調用原始對象的,而原始對象在去調用方法時,是不會再觸發代理了!

所以以上就是為什么我在沒有標注事務注解的方法A里去調用標注有事務注解的方法B而沒有事務滾回的原因;

看到這里,有的看官可能在想,你在方法A上標個注解不就完了嗎?為什么非要標注在方法B上?

由於我這里是循環更新數據,調用一次方法B就更新一次數據,涉及到幾張表,需要執行幾條update sql, 一條數據更新失敗不影響所有數據,所以說一條數據更新執行完畢后就提交一次事務,如果標注在方法A上,要所有的都執行完畢了才提交事務,這樣子是有問題滴.

原因:

spring 在掃描bean的時候會掃描方法上是否包含@Transactional注解,如果包含,spring會為這個bean動態地生成一個子類(即代理類,proxy),代理類是繼承原來那個bean的。此時,當這個有注解的方法被調用的時候,實際上是由代理類來調用的,代理類在調用之前就會啟動transaction。然而,如果這個有注解的方法是被同一個類中的其他方法調用的,那么該方法的調用並沒有通過代理類,而是直接通過原來的那個bean,所以就不會啟動transaction,我們看到的現象就是@Transactional注解無效。

為什么一個方法a()調用同一個類中另外一個方法b()的時候,b()不是通過代理類來調用的呢?可以看下面的例子(為了簡化,用偽代碼表示):

復制代碼
@Service
class A{
    @Transactinal
    method b(){...}
    
    method a(){    //標記1
        b();
    }
}
 
//Spring掃描注解后,創建了另外一個代理類,並為有注解的方法插入一個startTransaction()方法:
class proxy$A{
    A objectA = new A();
    method b(){    //標記2
        startTransaction();
        objectA.b();
    }
 
    method a(){    //標記3
        objectA.a();    //由於a()沒有注解,所以不會啟動transaction,而是直接調用A的實例的a()方法
    }
}
復制代碼

當我們調用A的bean的a()方法的時候,也是被proxyAproxyA攔截,執行proxyA.a()(標記3),然而,由以上代碼可知,這時候它調用的是objectA.a(),也就是由原來的bean來調用a()方法了,所以代碼跑到了“標記1”。由此可見,“標記2”並沒有被執行到,所以startTransaction()方法也沒有運行。


 方法A:無事務控制

 方法B:有事務控制

 方法B處理失敗手動拋出異常觸發回滾:

 方法A調用方法B:

從上圖可以看到,如果方法B中User更新出錯后需要回滾RedPacket數據,所以User更新失敗就拋出了繼承自RuntimeException的自定義異常,並且在調用方把這個異常catch到重新拋出,觸發事務回滾,但是並沒有執行;

下面是解決方案:

   1.把方法B抽離到另外一個XXService中去,並且在這個Service中注入XXService,使用XXService調用方法B;

      顯然,這種方式一點也不優雅,且要產生很多冗余文件,看起來很煩,實際開發中也幾乎沒人這么做吧?.反正我不建議采用此方案;

   2.通過在方法內部獲得當前類代理對象的方式,通過代理對象調用方法B

   上面說了:動態代理最終都是要調用原始對象的,而原始對象在去調用方法時,是不會再觸發代理了!

    所以我們就使用代理對象來調用,就會觸發事務;

綜上解決方案,我覺得第二種方式簡直方便到炸. 那怎么獲取代理對象呢? 這里提供兩種方式:

   1.使用 ApplicationContext 上下文對象獲取該對象;

   2.使用 AopContext.currentProxy() 獲取代理對象,但是需要配置exposeProxy=true

我這里使用的是第二種解決方案,具體操作如下:

springboot啟動類加上注解:@EnableAspectJAutoProxy(exposeProxy = true)

 方法內部獲取代理對象調用方法

 完了后再測試,數據順利回滾,至此,問題得到解決!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM