一個(gè)@Transaction哪里來這么多坑?
點(diǎn)擊藍(lán)色“程序員DMZ?”關(guān)注我喲
好看記得加個(gè)“星標(biāo)”哈!
前言
在之前的文章中已經(jīng)對(duì)Spring中的事務(wù)做了詳細(xì)的分析了,這篇文章我們來聊一聊平常工作時(shí)使用事務(wù)可能出現(xiàn)的一些問題(本文主要針對(duì)使用@Transactional進(jìn)行事務(wù)管理的方式進(jìn)行討論)以及對(duì)應(yīng)的解決方案
事務(wù)失效 事務(wù)回滾相關(guān)問題 讀寫分離跟事務(wù)結(jié)合使用時(shí)的問題
事務(wù)失效
事務(wù)失效我們一般要從兩個(gè)方面排查問題
數(shù)據(jù)庫層面
數(shù)據(jù)庫層面,數(shù)據(jù)庫使用的存儲(chǔ)引擎是否支持事務(wù)?默認(rèn)情況下MySQL數(shù)據(jù)庫使用的是Innodb存儲(chǔ)引擎(5.5版本之后),它是支持事務(wù)的,但是如果你的表特地修改了存儲(chǔ)引擎,例如,你通過下面的語句修改了表使用的存儲(chǔ)引擎為MyISAM,而MyISAM又是不支持事務(wù)的
alter?table?table_name?engine=myisam;
這樣就會(huì)出現(xiàn)“事務(wù)失效”的問題了
「解決方案」:修改存儲(chǔ)引擎為Innodb。
業(yè)務(wù)代碼層面
業(yè)務(wù)層面的代碼是否有問題,這就有很多種可能了
我們要使用Spring的聲明式事務(wù),那么需要執(zhí)行事務(wù)的Bean是否已經(jīng)交由了Spring管理?在代碼中的體現(xiàn)就是類上是否有 @Service、Component等一系列注解
「解決方案」:將Bean交由Spring進(jìn)行管理(添加@Service注解)
@Transactional注解是否被放在了合適的位置。在上篇文章中我們對(duì)Spring中事務(wù)失效的原理做了詳細(xì)的分析,其中也分析了Spring內(nèi)部是如何解析@Transactional注解的,我們稍微回顧下代碼:

?代碼位于:
?AbstractFallbackTransactionAttributeSource#computeTransactionAttribute中
也就是說,默認(rèn)情況下你無法使用@Transactional對(duì)一個(gè)非public的方法進(jìn)行事務(wù)管理
「解決方案」:修改需要事務(wù)管理的方法為public。
出現(xiàn)了自調(diào)用。什么是自調(diào)用呢?我們看個(gè)例子
@Service
public?class?DmzService?{
?
?public?void?saveAB(A?a,?B?b)?{
??saveA(a);
??saveB(b);
?}
?@Transactional
?public?void?saveA(A?a)?{
??dao.saveA(a);
?}
?
?@Transactional
?public?void?saveB(B?b){
??dao.saveB(a);
?}
}
上面三個(gè)方法都在同一個(gè)類DmzService中,其中saveAB方法中調(diào)用了本類中的saveA跟saveB方法,這就是自調(diào)用。在上面的例子中saveA跟saveB上的事務(wù)會(huì)失效
那么自調(diào)用為什么會(huì)導(dǎo)致事務(wù)失效呢?我們知道Spring中事務(wù)的實(shí)現(xiàn)是依賴于AOP的,當(dāng)容器在創(chuàng)建dmzService這個(gè)Bean時(shí),發(fā)現(xiàn)這個(gè)類中存在了被@Transactional標(biāo)注的方法(修飾符為public)那么就需要為這個(gè)類創(chuàng)建一個(gè)代理對(duì)象并放入到容器中,創(chuàng)建的代理對(duì)象等價(jià)于下面這個(gè)類
public?class?DmzServiceProxy?{
????private?DmzService?dmzService;
????public?DmzServiceProxy(DmzService?dmzService)?{
????????this.dmzService?=?dmzService;
????}
????public?void?saveAB(A?a,?B?b)?{
????????dmzService.saveAB(a,?b);
????}
????public?void?saveA(A?a)?{
????????try?{
????????????//?開啟事務(wù)
????????????startTransaction();
????????????dmzService.saveA(a);
????????}?catch?(Exception?e)?{
????????????//?出現(xiàn)異?;貪L事務(wù)
????????????rollbackTransaction();
????????}
????????//?提交事務(wù)
????????commitTransaction();
????}
????public?void?saveB(B?b)?{
????????try?{
????????????//?開啟事務(wù)
????????????startTransaction();
????????????dmzService.saveB(b);
????????}?catch?(Exception?e)?{
????????????//?出現(xiàn)異常回滾事務(wù)
????????????rollbackTransaction();
????????}
????????//?提交事務(wù)
????????commitTransaction();
????}
}
上面是一段偽代碼,通過startTransaction、rollbackTransaction、commitTransaction這三個(gè)方法模擬代理類實(shí)現(xiàn)的邏輯。因?yàn)槟繕?biāo)類DmzService中的saveA跟saveB方法上存在@Transactional注解,所以會(huì)對(duì)這兩個(gè)方法進(jìn)行攔截并嵌入事務(wù)管理的邏輯,同時(shí)saveAB方法上沒有@Transactional,相當(dāng)于代理類直接調(diào)用了目標(biāo)類中的方法。
我們會(huì)發(fā)現(xiàn)當(dāng)通過代理類調(diào)用saveAB時(shí)整個(gè)方法的調(diào)用鏈如下:

實(shí)際上我們?cè)谡{(diào)用saveA跟saveB時(shí)調(diào)用的是目標(biāo)類中的方法,這種清空下,事務(wù)當(dāng)然會(huì)失效。
常見的自調(diào)用導(dǎo)致的事務(wù)失效還有一個(gè)例子,如下:
@Service
public?class?DmzService?{
?@Transactional
?public?void?save(A?a,?B?b)?{
??saveB(b);
?}
?
?@Transactional(propagation?=?Propagation.REQUIRES_NEW)
?public?void?saveB(B?b){
??dao.saveB(a);
?}
}
當(dāng)我們調(diào)用save方法時(shí),我們預(yù)期的執(zhí)行流程是這樣的

也就是說兩個(gè)事務(wù)之間互不干擾,每個(gè)事務(wù)都有自己的開啟、回滾、提交操作。
但根據(jù)之前的分析我們知道,實(shí)際上在調(diào)用saveB方法時(shí),是直接調(diào)用的目標(biāo)類中的saveB方法,在saveB方法前后并不會(huì)有事務(wù)的開啟或者提交、回滾等操作,實(shí)際的流程是下面這樣的

由于saveB方法實(shí)際上是由dmzService也就是目標(biāo)類自己調(diào)用的,所以在saveB方法的前后并不會(huì)執(zhí)行事務(wù)的相關(guān)操作。這也是自調(diào)用帶來問題的根本原因:「自調(diào)用時(shí),調(diào)用的是目標(biāo)類中的方法而不是代理類中的方法」
「解決方案」:
自己注入自己,然后顯示的調(diào)用,例如:
@Service
public?class?DmzService?{
?//?自己注入自己
?@Autowired
?DmzService?dmzService;
?
?@Transactional
?public?void?save(A?a,?B?b)?{
??dmzService.saveB(b);
?}
?@Transactional(propagation?=?Propagation.REQUIRES_NEW)
?public?void?saveB(B?b){
??dao.saveB(a);
?}
}這種方案看起來不是很優(yōu)雅
利用
AopContext,如下:@Service
public?class?DmzService?{
?@Transactional
?public?void?save(A?a,?B?b)?{
??((DmzService)?AopContext.currentProxy()).saveB(b);
?}
?@Transactional(propagation?=?Propagation.REQUIRES_NEW)
?public?void?saveB(B?b){
??dao.saveB(a);
?}
}?
使用上面這種解決方案需要注意的是,需要在配置類上新增一個(gè)配置
?//?exposeProxy=true代表將代理類放入到線程上下文中,默認(rèn)是false
@EnableAspectJAutoProxy(exposeProxy?=?true)個(gè)人比較喜歡的是第二種方式
這里我們做個(gè)來做個(gè)小總結(jié)
總結(jié)
一圖勝千言

事務(wù)回滾相關(guān)問題
回滾相關(guān)的問題可以被總結(jié)為兩句話
想回滾的時(shí)候事務(wù)卻提交了 想提交的時(shí)候被標(biāo)記成只能回滾了(rollback only)
先看第一種情況:「想回滾的時(shí)候事務(wù)卻提交了」。這種情況往往是程序員對(duì)Spring中事務(wù)的rollbackFor屬性不夠了解導(dǎo)致的。
?Spring默認(rèn)拋出了未檢查
?unchecked異常(繼承自RuntimeException的異常)或者Error才回滾事務(wù);其他異常不會(huì)觸發(fā)回滾事務(wù),已經(jīng)執(zhí)行的SQL會(huì)提交掉。如果在事務(wù)中拋出其他類型的異常,但卻期望 Spring 能夠回滾事務(wù),就需要指定rollbackFor屬性。
對(duì)應(yīng)代碼其實(shí)我們上篇文章也分析過了,如下:

?以上代碼位于:
?TransactionAspectSupport#completeTransactionAfterThrowing方法中
默認(rèn)情況下,只有出現(xiàn)RuntimeException或者Error才會(huì)回滾
public?boolean?rollbackOn(Throwable?ex)?{
????return?(ex?instanceof?RuntimeException?||?ex?instanceof?Error);
}
所以,如果你想在出現(xiàn)了非RuntimeException或者Error時(shí)也回滾,請(qǐng)指定回滾時(shí)的異常,例如:
@Transactional(rollbackFor?=?Exception.class)
第二種情況:「想提交的時(shí)候被標(biāo)記成只能回滾了(rollback only)」。
對(duì)應(yīng)的異常信息如下:
Transaction?rolled?back?because?it?has?been?marked?as?rollback-only
我們先來看個(gè)例子吧
@Service
public?class?DmzService?{
?@Autowired
?IndexService?indexService;
?@Transactional
?public?void?testRollbackOnly()?{
??try?{
???indexService.a();
??}?catch?(ClassNotFoundException?e)?{
???System.out.println("catch");
??}
?}
}
@Service
public?class?IndexService?{
?@Transactional(rollbackFor?=?Exception.class)
?public?void?a()?throws?ClassNotFoundException{
??//?......
??throw?new?ClassNotFoundException();
?}
}
在上面這個(gè)例子中,DmzService的testRollbackOnly方法跟IndexService的a方法都開啟了事務(wù),并且事務(wù)的傳播級(jí)別為required,所以當(dāng)我們?cè)?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">testRollbackOnly中調(diào)用IndexService的a方法時(shí)這兩個(gè)方法應(yīng)當(dāng)是共用的一個(gè)事務(wù)。按照這種思路,雖然IndexService的a方法拋出了異常,但是我們?cè)?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">testRollbackOnly將異常捕獲了,那么這個(gè)事務(wù)應(yīng)該是可以正常提交的,為什么會(huì)拋出異常呢?
如果你看過我之前的源碼分析的文章應(yīng)該知道,在處理回滾時(shí)有這么一段代碼

在提交時(shí)又做了下面這個(gè)判斷(這個(gè)方法我刪掉了一些不重要的代碼)

可以看到當(dāng)提交時(shí)發(fā)現(xiàn)事務(wù)已經(jīng)被標(biāo)記為rollbackOnly后會(huì)進(jìn)入回滾處理中,并且unexpected傳入的為true。在處理回滾時(shí)又有下面這段代碼

最后在這里拋出了這個(gè)異常。
?以上代碼均位于
?AbstractPlatformTransactionManager中
總結(jié)起來,「主要的原因就是因?yàn)閮?nèi)部事務(wù)回滾時(shí)將整個(gè)大事務(wù)做了一個(gè)rollbackOnly的標(biāo)記」,所以即使我們?cè)谕獠渴聞?wù)中catch了拋出的異常,整個(gè)事務(wù)仍然無法正常提交,并且如果你希望正常提交,Spring還會(huì)拋出一個(gè)異常。
「解決方案」:
這個(gè)解決方案要依賴業(yè)務(wù)而定,你要明確你想要的結(jié)果是什么
內(nèi)部事務(wù)發(fā)生異常,外部事務(wù)catch異常后,內(nèi)部事務(wù)自行回滾,不影響外部事務(wù)
?將內(nèi)部事務(wù)的傳播級(jí)別設(shè)置為nested/requires_new均可。在我們的例子中就是做如下修改:
?//?@Transactional(rollbackFor?=?Exception.class,propagation?=?Propagation.REQUIRES_NEW)
@Transactional(rollbackFor?=?Exception.class,propagation?=?Propagation.NESTED)
public?void?a()?throws?ClassNotFoundException{
???//?......
???throw?new?ClassNotFoundException();
}
雖然這兩者都能得到上面的結(jié)果,但是它們之間還是有不同的。當(dāng)傳播級(jí)別為requires_new時(shí),兩個(gè)事務(wù)完全沒有聯(lián)系,各自都有自己的事務(wù)管理機(jī)制(開啟事務(wù)、關(guān)閉事務(wù)、回滾事務(wù))。但是傳播級(jí)別為nested時(shí),實(shí)際上只存在一個(gè)事務(wù),只是在調(diào)用a方法時(shí)設(shè)置了一個(gè)保存點(diǎn),當(dāng)a方法回滾時(shí),實(shí)際上是回滾到保存點(diǎn)上,并且當(dāng)外部事務(wù)提交時(shí),內(nèi)部事務(wù)才會(huì)提交,外部事務(wù)如果回滾,內(nèi)部事務(wù)會(huì)跟著回滾。
內(nèi)部事務(wù)發(fā)生異常時(shí),外部事務(wù)catch異常后,內(nèi)外兩個(gè)事務(wù)都回滾,但是方法不拋出異常
??@Transactional
public?void?testRollbackOnly()?{
???try?{
??????indexService.a();
???}?catch?(ClassNotFoundException?e)?{
??????//?加上這句代碼
??????TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
???}
}
通過顯示的設(shè)置事務(wù)的狀態(tài)為RollbackOnly。這樣當(dāng)提交事務(wù)時(shí)會(huì)進(jìn)入下面這段代碼

最大的區(qū)別在于處理回滾時(shí)第二個(gè)參數(shù)傳入的是false,這意味著回滾是回滾是預(yù)期之中的,所以在處理完回滾后并不會(huì)拋出異常。
讀寫分離跟事務(wù)結(jié)合使用時(shí)的問題
讀寫分離一般有兩種實(shí)現(xiàn)方式
配置多數(shù)據(jù)源 依賴中間件,如 MyCat
如果是配置了多數(shù)據(jù)源的方式實(shí)現(xiàn)了讀寫分離,那么需要注意的是:「如果開啟了一個(gè)讀寫事務(wù),那么必須使用寫節(jié)點(diǎn)」,「如果是一個(gè)只讀事務(wù),那么可以使用讀節(jié)點(diǎn)」
如果是依賴于MyCat等中間件那么需要注意:「只要開啟了事務(wù),事務(wù)內(nèi)的SQL都會(huì)使用寫節(jié)點(diǎn)(依賴于具體中間件的實(shí)現(xiàn),也有可能會(huì)允許使用讀節(jié)點(diǎn),具體策略需要自行跟DB團(tuán)隊(duì)確認(rèn))」
基于上面的結(jié)論,我們?cè)谑褂檬聞?wù)時(shí)應(yīng)該更加謹(jǐn)慎,在沒有必要開啟事務(wù)時(shí)盡量不要開啟。
?一般我們會(huì)在配置文件配置某些約定的方法名字前綴開啟不同的事務(wù)(或者不開啟),但現(xiàn)在隨著注解事務(wù)的流行,好多開發(fā)人員(或者架構(gòu)師)搭建框架的時(shí)候在service類上加上了@Transactional注解,導(dǎo)致整個(gè)類都是開啟事務(wù)的,這樣嚴(yán)重影響數(shù)據(jù)庫執(zhí)行的效率,更重要的是開發(fā)人員不重視、或者不知道在查詢類的方法上面自己加上@Transactional(propagation=Propagation.NOT_SUPPORTED)就會(huì)導(dǎo)致,所有的查詢方法實(shí)際并沒有走從庫,導(dǎo)致主庫壓力過大。
?
其次,關(guān)于如果沒有對(duì)只讀事務(wù)做優(yōu)化的話(優(yōu)化意味著將只讀事務(wù)路由到讀節(jié)點(diǎn)),那么@Transactional注解中的readOnly屬性就應(yīng)該要慎用。我們使用readOnly的原本目的是為了將事務(wù)標(biāo)記為只讀,這樣當(dāng)MySQL服務(wù)端檢測到是一個(gè)只讀事務(wù)后就可以做優(yōu)化,少分配一些資源(例如:只讀事務(wù)不需要回滾,所以不需要分配undo log段)。但是當(dāng)配置了讀寫分離后,可能會(huì)可能會(huì)導(dǎo)致只讀事務(wù)內(nèi)所有的SQL都被路由到了主庫,讀寫分離也就失去了意義。
總結(jié)
本文為事務(wù)專欄最后一篇啦!這篇文章主要是總結(jié)了工作中事務(wù)相關(guān)的常見問題,想讓大家少走點(diǎn)彎路!希望大家可以認(rèn)真讀完哦,有什么問題可以直接在后臺(tái)私信我或者加我微信!
這篇文章也是整個(gè)Spring系列的最后一篇文章,之后可能會(huì)出一篇源碼閱讀心得,跟大家聊聊如何學(xué)習(xí)源碼。
另外今年也給自己定了個(gè)小目標(biāo),就是完成SSM框架源碼的閱讀。目前來說Spring是完成,接下來就是SpringMVC跟MyBatis。
在分析MyBatis前,會(huì)從JDBC源碼出發(fā),然后就是MyBatis對(duì)配置的解析、MyBatis執(zhí)行流程、MyBatis的緩存、MyBatis的事務(wù)管理以及MyBatis的插件機(jī)制。
在學(xué)習(xí)SpringMVC前,會(huì)從TomCat出發(fā),先講清楚TomCat的原理,我們?cè)賮砜?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">SpringMVC。整個(gè)來說相比于Spring源碼,我覺得應(yīng)該不算特別難。
希望在這個(gè)過程中可以跟大家一起進(jìn)步?。?!
我叫DMZ,一個(gè)陪你一起慢慢進(jìn)步的小菜鳥~!
