@Transactional千萬不要這樣用,踩坑了你都可能發(fā)現(xiàn)不了?。?!

前陣子接手了一段同事之前的代碼,里面用到了@transaction注解,了解Spring的小伙伴肯定知道,@Transactional是Spring提供的一種控制事務(wù)管理的快捷手段。但是我這段程序在運行的時候,經(jīng)常出現(xiàn)莫名其妙的問題,連夜研究了好久才搞清楚,在這里記錄一下,避免大家入坑。

1. 大家來找茬

在介紹具體問題之前,我把問題代碼簡化了一下,看大家能找到其中的問題嗎?

問題代碼1

下面的這段代碼主要是想利用MySQL里面的行鎖select for update,來實現(xiàn)簡單的分布式鎖。但是在實踐過程中,發(fā)現(xiàn)這個鎖好像并沒有生效,而且在數(shù)據(jù)庫的里面也沒有查找對應(yīng)transaction連接的信息。

@Component

@EnableScheduling

public class someService {


?@Scheduled(...)

?public doSomeWork() {

? ?// find some id by logic


? ?// process the related info

? ?doOtherWork(id);

? }


?@Transactional(isolation = Isolation.READ_COMMITTED)

?public void doOtherWork(id) {

? ?Info info = requestMapper.selectByPrimaryKeyForUpdate(id);

? ?doSomeFollowingProcess(info);

?? ...

? }

}

問題代碼2

下面代碼分兩個步驟,第一步會檢查相關(guān)信息,第二步調(diào)用了一個transactional修飾的方法,完成一些基本工作;但在實踐中,發(fā)現(xiàn)一個非常詭異的問題,在MainWork中,doSomeCheck執(zhí)行時會拋出nullPointException,debug發(fā)現(xiàn)所有autowired進來的service均為空,注釋掉doSomeCheck里面的內(nèi)容后,繼續(xù)往下執(zhí)行,卻發(fā)現(xiàn)doWork能夠正常執(zhí)行,所有的注入均沒有問題。

@Component

public class MainWork {

?@AutoWired

?DetailWork detailWork


?public void workflow() {

? ?detailWork.doSomeCheck();

? ?detailWork.doWork();

? }

}

@Component

public class DetailWork {


? ? @AutoWired

? ? UsefulService usefulService;


? ? @AutoWired

? ? InfoService infoService;


? ?@Transactional(isolation = Isolation.READ_COMMITTED)

? ? public void doWork() {

? ? ?usefulService.doSomeWork();

?? }


? ? void doSomeCheck() {

? ? ?infoService.getInfo();

?? }

}

大伙看看能發(fā)現(xiàn)什么問題嗎?

2. 關(guān)于@Transactional注解

Spring支持編程式事務(wù)管理聲明式事務(wù)管理兩種方式。

編程式事務(wù)管理使用TransactionTemplate或者直接使用底層的PlatformTransactionManager。

聲明式事務(wù)管理建立在AOP之上的。其本質(zhì)是對方法前后進行攔截,然后在目標(biāo)方法開始之前創(chuàng)建或者加入一個事務(wù),在執(zhí)行完目標(biāo)方法之后,根據(jù)執(zhí)行情況提交或者回滾事務(wù)。聲明式事務(wù)最大的優(yōu)點就是不需要通過編程的方式管理事務(wù),這樣就不需要在業(yè)務(wù)邏輯代碼中摻雜事務(wù)管理的代碼,只需基于@Transactional注解的方式,便可以將事務(wù)規(guī)則應(yīng)用到業(yè)務(wù)邏輯中

下圖是調(diào)用@Transactional注解的方法時,Spring內(nèi)部的時序圖。簡單來講就是IOC容器初始化時,會生成@Transactional注解所在類的代理對象,然后實際執(zhí)行中會通過AOP執(zhí)行代理對象的方法,TransactionAdvisor會在方法調(diào)用前判斷是否開啟事務(wù),在調(diào)用結(jié)束后,會判斷是否提交或回滾事務(wù)。


深入研究代碼,我們會發(fā)現(xiàn)TransactionInterceptor (事務(wù)攔截器)在目標(biāo)方法執(zhí)行前后進行攔截,DynamicAdvisedInterceptor(CglibAopProxy 的內(nèi)部類)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法會間接調(diào)用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,獲取Transactional 注解的事務(wù)配置信息。

protected TransactionAttribute computeTransactionAttribute(Method method,

? ?Class<?> targetClass) {

? ? ? ?// Don't allow no-public methods as required.

? ? ? ?if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {

? ? ? ?return null;

}

此方法會檢查目標(biāo)方法的修飾符是否為 public,不是 public則不會獲取@Transactional 的屬性配置信息。也就是說protected、private 修飾的方法上使用 @Transactional 注解會導(dǎo)致事務(wù)無效。

了解了@Transactional的原理之后,我們在回頭看看之前的問題,會不會是使用方法不對導(dǎo)致的呢?

3. 撥云見日

問題代碼1解析

下面的代碼中,我們在同一個類里面調(diào)用了@Transactional修飾的方法,其實這樣調(diào)用的話并沒有用到Spring AOP生成的代理對象。從上面的時序圖也可以看到,只有當(dāng)事務(wù)方法被當(dāng)前類以外的代碼調(diào)用時,才會由Spring生成的代理對象來管理。

@Component

@EnableScheduling

public class someService {


?@Scheduled(...)

?public doSomeWork() {

? ?// find some id by logic


? ?// process the related info

? ?doOtherWork(id);

? }


?@Transactional(isolation = Isolation.READ_COMMITTED)

?public void doOtherWork(id) {

? ?Info info = requestMapper.selectByPrimaryKeyForUpdate(id);

? ?doSomeFollowingProcess(info);

?? ...

? }

}

那如何解決這種類內(nèi)調(diào)用的問題呢? 很簡單,可以使用applicationContext直接從IOC容器中將someService類取出來,然后再調(diào)用doOtherWork方法即可,這樣就能用上Spring AOP生成的代理對象了。

下面是更改之后的代碼,更改之后發(fā)現(xiàn)事務(wù)生效了,問題解決!

@Component

@EnableScheduling

public class someService {


?@Autowired

?private ApplicationContext applicationContext;


?@Scheduled(...)

?public doSomeWork() {

? ?// find some id by logic


? ?// process the related info

? ?SomeService someService = applicationContext.getBean(someService.class);

? ?someService.doOtherWork(id);

? }


?@Transactional(isolation = Isolation.READ_COMMITTED)

?public void doOtherWork(id) {

? ?Info info = requestMapper.selectByPrimaryKeyForUpdate(id);

? ?doSomeFollowingProcess(info);

?? ...

? }

}

問題代碼2解析

下面的代碼中,MainWork調(diào)用doSomeCheck的時候,會出現(xiàn)null的情況,原因是由于該方法不是public方法,會導(dǎo)致@Transactional調(diào)用失敗。你可能會說這就是普通方法,跟@Transactional有什么關(guān)系?

需要注意的是,無論transactional注解在類上還是在方法上,IOC容器都會生成對應(yīng)類的代理對象,然后使用代理對象去訪問對應(yīng)的方法。在這個例子里面, 調(diào)用doWork時一切正常,事務(wù)也會生效;但是調(diào)用doSomeCheck時,從之前的分析可以看到,由于方法不是public,此時事務(wù)管理器不會起作用,直接導(dǎo)致所有的autowired未完成注入。修改的方法也很簡單,把doSomeCheck改成public就行了。

這個問題隱藏比較深一些,不清楚原理很難發(fā)現(xiàn)這個問題。

@Component

public class MainWork {

?@AutoWired

?DetailWork detailWork


?public void workflow() {

? ?detailWork.doSomeCheck();

? ?detailWork.doWork();

? }

}

@Component

public class DetailWork {


? ? @AutoWired

? ? UsefulService usefulService;


? ? @AutoWired

? ? InfoService infoService;


? ?@Transactional(isolation = Isolation.READ_COMMITTED)

? ? public void doWork() {

? ? ?usefulService.doSomeWork();

?? }


? ? public void doSomeCheck() {

? ? ?infoService.getInfo();

?? }

}

4. 相關(guān)拓展

幾種事務(wù)失效的場景

上面說到的兩個問題,其實就是@Transactional注解使用不當(dāng),導(dǎo)致失效的兩種情形;除此之外,以下幾種情況也會導(dǎo)致事務(wù)失效:

業(yè)務(wù)代碼中存在異常時,使用try…catch…語句塊捕獲,而catch語句塊沒有throw new RuntimeExecption異常;(最難被排查到問題且容易忽略)

注解@Transactional中Propagation屬性值設(shè)置錯誤即Propagation.NOT_SUPPORTED(一般不會設(shè)置此種傳播機制)

mysql關(guān)系型數(shù)據(jù)庫,且存儲引擎是MyISAM而非InnoDB,則事務(wù)會不起作用(比較少見);

業(yè)務(wù)代碼拋出異常類型非RuntimeException,事務(wù)失效;Spring默認(rèn)拋出未檢查unchecked異常(繼承自RuntimeException的異常)或者Error才回滾事務(wù);其他異常不會觸發(fā)回滾事務(wù)。如果在事務(wù)中拋出其他類型的異常,但卻期望 Spring 能夠回滾事務(wù),就需要指定rollbackFor屬性。


事務(wù)的傳播行為

事務(wù)的傳播行為也會影響到事務(wù)與事務(wù)之間的關(guān)系,一定要搞清楚,否則經(jīng)常會出現(xiàn)很奇怪的問題。

具體來講有以下幾種屬性:

propagation 代表事務(wù)的傳播行為,默認(rèn)值為 Propagation.REQUIRED,其他的屬性信息如下:

Propagation.REQUIRED:如果當(dāng)前存在事務(wù),則加入該事務(wù),如果當(dāng)前不存在事務(wù),則創(chuàng)建一個新的事務(wù)。( 也就是說如果A方法和B方法都添加了注解,在默認(rèn)傳播模式下,A方法內(nèi)部調(diào)用B方法,會把兩個方法的事務(wù)合并為一個事務(wù) )

Propagation.SUPPORTS:如果當(dāng)前存在事務(wù),則加入該事務(wù);如果當(dāng)前不存在事務(wù),則以非事務(wù)的方式繼續(xù)運行。

Propagation.MANDATORY:如果當(dāng)前存在事務(wù),則加入該事務(wù);如果當(dāng)前不存在事務(wù),則拋出異常。

Propagation.REQUIRES_NEW:重新創(chuàng)建一個新的事務(wù),如果當(dāng)前存在事務(wù),暫停當(dāng)前的事務(wù)。( 當(dāng)類A中的 a 方法用默Propagation.REQUIRED模式,類B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中調(diào)用 b方法操作數(shù)據(jù)庫,然而 a方法拋出異常后,b方法并沒有進行回滾,因為Propagation.REQUIRES_NEW會暫停 a方法的事務(wù) )

Propagation.NOT_SUPPORTED:以非事務(wù)的方式運行,如果當(dāng)前存在事務(wù),暫停當(dāng)前的事務(wù)。

Propagation.NEVER:以非事務(wù)的方式運行,如果當(dāng)前存在事務(wù),則拋出異常。

Propagation.NESTED :和 Propagation.REQUIRED 效果一樣。

事務(wù)的隔離級別

SQL標(biāo)準(zhǔn)定義了4種事務(wù)隔離級別來避免3種數(shù)據(jù)不一致的問題。事務(wù)等級從高到低,分別為:

1.Serializable(序列化)

系統(tǒng)中所有的事務(wù)以串行地方式逐個執(zhí)行,所以能避免所有數(shù)據(jù)不一致情況。

但是這種以排他方式來控制并發(fā)事務(wù),串行化執(zhí)行方式會導(dǎo)致事務(wù)排隊,系統(tǒng)的并發(fā)量大幅下降,使用的時候要絕對慎重。

2.Repeatable read(可重復(fù)讀)

一個事務(wù)一旦開始,事務(wù)過程中所讀取的所有數(shù)據(jù)不允許被其他事務(wù)修改。

一個隔離級別沒有辦法解決“幻影讀”的問題。

因為它只“保護”了它讀取的數(shù)據(jù)不被修改,但是其他數(shù)據(jù)會被修改。如果其他數(shù)據(jù)被修改后恰好滿足了當(dāng)前事務(wù)的過濾條件(where語句),那么就會發(fā)生“幻影讀”的情況。

其他兩種事務(wù)隔離等級為:

3.Read Committed(已提交讀)

一個事務(wù)能讀取到其他事務(wù)提交過(Committed)的數(shù)據(jù)。

一個事務(wù)在處理過程中如果重復(fù)讀取某一個數(shù)據(jù),而且這個數(shù)據(jù)恰好被其他事務(wù)修改并提交了,那么當(dāng)前重復(fù)讀取數(shù)據(jù)的事務(wù)就會出現(xiàn)同一個數(shù)據(jù)前后不同的情況。

在這個隔離級別會發(fā)生“不可重復(fù)讀”的場景。

4.Read Uncommitted(未提交讀)

一個事務(wù)能讀取到其他事務(wù)修改過,但是還沒有提交的(Uncommitted)的數(shù)據(jù)。

數(shù)據(jù)被其他事務(wù)修改過,但還沒有提交,就存在著回滾的可能性,這時候讀取這些“未提交”數(shù)據(jù)的情況就是“臟讀”。

在這個隔離級別會發(fā)生“臟讀”場景。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容