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

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

實際上我們在調(diào)用saveA跟saveB時調(diào)用的是目標類中的方法,這種清空下,事務(wù)當然會失效。
常見的自調(diào)用導(dǎo)致的事務(wù)失效還有一個例子,如下:
@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);
?}
}
當我們調(diào)用save方法時,我們預(yù)期的執(zhí)行流程是這樣的

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

由于saveB方法實際上是由dmzService也就是目標類自己調(diào)用的,所以在saveB方法的前后并不會執(zhí)行事務(wù)的相關(guān)操作。這也是自調(diào)用帶來問題的根本原因:「自調(diào)用時,調(dià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);
?}
}?使用上面這種解決方案需要注意的是,需要在配置類上新增一個配置 //?exposeProxy=true代表將代理類放入到線程上下文中,默認是false
@EnableAspectJAutoProxy(exposeProxy?=?true)? 個人比較喜歡的是第二種方式
總結(jié)

事務(wù)回滾相關(guān)問題
想回滾的時候事務(wù)卻提交了 想提交的時候被標記成只能回滾了(rollback only)
rollbackFor屬性不夠了解導(dǎo)致的。unchecked異常(繼承自 RuntimeException 的異常)或者 Error才回滾事務(wù);其他異常不會觸發(fā)回滾事務(wù),已經(jīng)執(zhí)行的SQL會提交掉。如果在事務(wù)中拋出其他類型的異常,但卻期望 Spring 能夠回滾事務(wù),就需要指定rollbackFor屬性。?
TransactionAspectSupport#completeTransactionAfterThrowing方法中?RuntimeException或者Error才會回滾public?boolean?rollbackOn(Throwable?ex)?{
????return?(ex?instanceof?RuntimeException?||?ex?instanceof?Error);
}
RuntimeException或者Error時也回滾,請指定回滾時的異常,例如:@Transactional(rollbackFor?=?Exception.class)
Transaction?rolled?back?because?it?has?been?marked?as?rollback-only
@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();
?}
}
DmzService的testRollbackOnly方法跟IndexService的a方法都開啟了事務(wù),并且事務(wù)的傳播級別為required,所以當我們在testRollbackOnly中調(diào)用IndexService的a方法時這兩個方法應(yīng)當是共用的一個事務(wù)。按照這種思路,雖然IndexService的a方法拋出了異常,但是我們在testRollbackOnly將異常捕獲了,那么這個事務(wù)應(yīng)該是可以正常提交的,為什么會拋出異常呢?


AbstractPlatformTransactionManager中?內(nèi)部事務(wù)發(fā)生異常,外部事務(wù)catch異常后,內(nèi)部事務(wù)自行回滾,不影響外部事務(wù)
//?@Transactional(rollbackFor?=?Exception.class,propagation?=?Propagation.REQUIRES_NEW)
@Transactional(rollbackFor?=?Exception.class,propagation?=?Propagation.NESTED)
public?void?a()?throws?ClassNotFoundException{
???//?......
???throw?new?ClassNotFoundException();
}
requires_new時,兩個事務(wù)完全沒有聯(lián)系,各自都有自己的事務(wù)管理機制(開啟事務(wù)、關(guān)閉事務(wù)、回滾事務(wù))。但是傳播級別為nested時,實際上只存在一個事務(wù),只是在調(diào)用a方法時設(shè)置了一個保存點,當a方法回滾時,實際上是回滾到保存點上,并且當外部事務(wù)提交時,內(nèi)部事務(wù)才會提交,外部事務(wù)如果回滾,內(nèi)部事務(wù)會跟著回滾。內(nèi)部事務(wù)發(fā)生異常時,外部事務(wù)catch異常后,內(nèi)外兩個事務(wù)都回滾,但是方法不拋出異常
@Transactional
public?void?testRollbackOnly()?{
???try?{
??????indexService.a();
???}?catch?(ClassNotFoundException?e)?{
??????//?加上這句代碼
??????TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
???}
}
RollbackOnly。這樣當提交事務(wù)時會進入下面這段代碼
?
讀寫分離跟事務(wù)結(jié)合使用時的問題
配置多數(shù)據(jù)源 依賴中間件,如 MyCat
MyCat等中間件那么需要注意:「只要開啟了事務(wù),事務(wù)內(nèi)的SQL都會使用寫節(jié)點(依賴于具體中間件的實現(xiàn),也有可能會允許使用讀節(jié)點,具體策略需要自行跟DB團隊確認)」@Transactional注解中的readOnly屬性就應(yīng)該要慎用。我們使用readOnly的原本目的是為了將事務(wù)標記為只讀,這樣當MySQL服務(wù)端檢測到是一個只讀事務(wù)后就可以做優(yōu)化,少分配一些資源(例如:只讀事務(wù)不需要回滾,所以不需要分配undo log段)。但是當配置了讀寫分離后,可能會可能會導(dǎo)致只讀事務(wù)內(nèi)所有的SQL都被路由到了主庫,讀寫分離也就失去了意義。總結(jié)
SpringMVC。整個來說相比于Spring源碼,我覺得應(yīng)該不算特別難。有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
