多方法调用 一个出错 集体回滚_一个@Transaction哪里来这么多坑?
前言
在之前的文章中已經對Spring中的事務做了詳細的分析了,這篇文章我們來聊一聊平常工作時使用事務可能出現的一些問題(本文主要針對使用@Transactional進行事務管理的方式進行討論)以及對應的解決方案
事務失效
事務回滾相關問題
讀寫分離跟事務結合使用時的問題
事務失效
事務失效我們一般要從兩個方面排查問題
數據庫層面
數據庫層面,數據庫使用的存儲引擎是否支持事務?默認情況下MySQL數據庫使用的是Innodb存儲引擎(5.5版本之后),它是支持事務的,但是如果你的表特地修改了存儲引擎,例如,你通過下面的語句修改了表使用的存儲引擎為MyISAM,而MyISAM又是不支持事務的
alter?table?table_name?engine=myisam;
這樣就會出現“事務失效”的問題了
「解決方案」:修改存儲引擎為Innodb。
業務代碼層面
業務層面的代碼是否有問題,這就有很多種可能了
我們要使用Spring的聲明式事務,那么需要執行事務的Bean是否已經交由了Spring管理?在代碼中的體現就是類上是否有@Service、Component等一系列注解
「解決方案」:將Bean交由Spring進行管理(添加@Service注解)
@Transactional注解是否被放在了合適的位置。在上篇文章中我們對Spring中事務失效的原理做了詳細的分析,其中也分析了Spring內部是如何解析@Transactional注解的,我們稍微回顧下代碼:
?代碼位于:AbstractFallbackTransactionAttributeSource#computeTransactionAttribute中?
也就是說,默認情況下你無法使用@Transactional對一個非public的方法進行事務管理
「解決方案」:修改需要事務管理的方法為public。
出現了自調用。什么是自調用呢?我們看個例子
@Servicepublic?class?DmzService?{public?void?saveAB(A?a,?B?b)?{
??saveA(a);
??saveB(b);
?}@Transactionalpublic?void?saveA(A?a)?{
??dao.saveA(a);
?}@Transactionalpublic?void?saveB(B?b){
??dao.saveB(a);
?}
}
上面三個方法都在同一個類DmzService中,其中saveAB方法中調用了本類中的saveA跟saveB方法,這就是自調用。在上面的例子中saveA跟saveB上的事務會失效
那么自調用為什么會導致事務失效呢?我們知道Spring中事務的實現是依賴于AOP的,當容器在創建dmzService這個Bean時,發現這個類中存在了被@Transactional標注的方法(修飾符為public)那么就需要為這個類創建一個代理對象并放入到容器中,創建的代理對象等價于下面這個類
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?{//?開啟事務
????????????startTransaction();
????????????dmzService.saveA(a);
????????}?catch?(Exception?e)?{//?出現異常回滾事務
????????????rollbackTransaction();
????????}//?提交事務
????????commitTransaction();
????}public?void?saveB(B?b)?{try?{//?開啟事務
????????????startTransaction();
????????????dmzService.saveB(b);
????????}?catch?(Exception?e)?{//?出現異常回滾事務
????????????rollbackTransaction();
????????}//?提交事務
????????commitTransaction();
????}
}
上面是一段偽代碼,通過startTransaction、rollbackTransaction、commitTransaction這三個方法模擬代理類實現的邏輯。因為目標類DmzService中的saveA跟saveB方法上存在@Transactional注解,所以會對這兩個方法進行攔截并嵌入事務管理的邏輯,同時saveAB方法上沒有@Transactional,相當于代理類直接調用了目標類中的方法。
我們會發現當通過代理類調用saveAB時整個方法的調用鏈如下:
實際上我們在調用saveA跟saveB時調用的是目標類中的方法,這種清空下,事務當然會失效。
常見的自調用導致的事務失效還有一個例子,如下:
@Servicepublic?class?DmzService?{@Transactionalpublic?void?save(A?a,?B?b)?{
??saveB(b);
?}@Transactional(propagation?=?Propagation.REQUIRES_NEW)public?void?saveB(B?b){
??dao.saveB(a);
?}
}
當我們調用save方法時,我們預期的執行流程是這樣的
也就是說兩個事務之間互不干擾,每個事務都有自己的開啟、回滾、提交操作。
但根據之前的分析我們知道,實際上在調用saveB方法時,是直接調用的目標類中的saveB方法,在saveB方法前后并不會有事務的開啟或者提交、回滾等操作,實際的流程是下面這樣的
由于saveB方法實際上是由dmzService也就是目標類自己調用的,所以在saveB方法的前后并不會執行事務的相關操作。這也是自調用帶來問題的根本原因:「自調用時,調用的是目標類中的方法而不是代理類中的方法」
「解決方案」:
?DmzService?dmzService;@Transactionalpublic?void?save(A?a,?B?b)?{
??dmzService.saveB(b);
?}@Transactional(propagation?=?Propagation.REQUIRES_NEW)public?void?saveB(B?b){
??dao.saveB(a);
?}
}這種方案看起來不是很優雅利用AopContext,如下:@Servicepublic?class?DmzService?{@Transactionalpublic?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)?個人比較喜歡的是第二種方式
總結
一圖勝千言事務失效的原因事務回滾相關問題
回滾相關的問題可以被總結為兩句話}所以,如果你想在出現了非RuntimeException或者Error時也回滾,請指定回滾時的異常,例如:@Transactional(rollbackFor?=?Exception.class)第二種情況:「想提交的時候被標記成只能回滾了(rollback only)」。對應的異常信息如下:Transaction?rolled?back?because?it?has?been?marked?as?rollback-only我們先來看個例子吧@Servicepublic?class?DmzService?{@Autowired
?IndexService?indexService;@Transactionalpublic?void?testRollbackOnly()?{try?{
???indexService.a();
??}?catch?(ClassNotFoundException?e)?{
???System.out.println("catch");
??}
?}
}@Servicepublic?class?IndexService?{@Transactional(rollbackFor?=?Exception.class)public?void?a()?throws?ClassNotFoundException{//?......throw?new?ClassNotFoundException();
?}
}在上面這個例子中,DmzService的testRollbackOnly方法跟IndexService的a方法都開啟了事務,并且事務的傳播級別為required,所以當我們在testRollbackOnly中調用IndexService的a方法時這兩個方法應當是共用的一個事務。按照這種思路,雖然IndexService的a方法拋出了異常,但是我們在testRollbackOnly將異常捕獲了,那么這個事務應該是可以正常提交的,為什么會拋出異常呢?如果你看過我之前的源碼分析的文章應該知道,在處理回滾時有這么一段代碼rollBackOnly設置在提交時又做了下面這個判斷(這個方法我刪掉了一些不重要的代碼)commit_rollbackOnly可以看到當提交時發現事務已經被標記為rollbackOnly后會進入回滾處理中,并且unexpected傳入的為true。在處理回滾時又有下面這段代碼拋出異常最后在這里拋出了這個異常。?以上代碼均位于AbstractPlatformTransactionManager中?總結起來,「主要的原因就是因為內部事務回滾時將整個大事務做了一個rollbackOnly的標記」,所以即使我們在外部事務中catch了拋出的異常,整個事務仍然無法正常提交,并且如果你希望正常提交,Spring還會拋出一個異常。「解決方案」:這個解決方案要依賴業務而定,你要明確你想要的結果是什么
}?雖然這兩者都能得到上面的結果,但是它們之間還是有不同的。當傳播級別為requires_new時,兩個事務完全沒有聯系,各自都有自己的事務管理機制(開啟事務、關閉事務、回滾事務)。但是傳播級別為nested時,實際上只存在一個事務,只是在調用a方法時設置了一個保存點,當a方法回滾時,實際上是回滾到保存點上,并且當外部事務提交時,內部事務才會提交,外部事務如果回滾,內部事務會跟著回滾。
??????indexService.a();
???}?catch?(ClassNotFoundException?e)?{//?加上這句代碼
??????TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
???}
}?通過顯示的設置事務的狀態為RollbackOnly。這樣當提交事務時會進入下面這段代碼顯示回滾最大的區別在于處理回滾時第二個參數傳入的是false,這意味著回滾是回滾是預期之中的,所以在處理完回滾后并不會拋出異常。
?
讀寫分離跟事務結合使用時的問題
讀寫分離一般有兩種實現方式總結
本文為事務專欄最后一篇啦!這篇文章主要是總結了工作中事務相關的常見問題,想讓大家少走點彎路!希望大家可以認真讀完哦,有什么問題可以直接在后臺私信我或者加我微信!這篇文章也是整個Spring系列的最后一篇文章,之后可能會出一篇源碼閱讀心得,跟大家聊聊如何學習源碼。另外今年也給自己定了個小目標,就是完成SSM框架源碼的閱讀。目前來說Spring是完成,接下來就是SpringMVC跟MyBatis。在分析MyBatis前,會從JDBC源碼出發,然后就是MyBatis對配置的解析、MyBatis執行流程、MyBatis的緩存、MyBatis的事務管理以及MyBatis的插件機制。在學習SpringMVC前,會從TomCat出發,先講清楚TomCat的原理,我們再來看SpringMVC。整個來說相比于Spring源碼,我覺得應該不算特別難。有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的多方法调用 一个出错 集体回滚_一个@Transaction哪里来这么多坑?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: idea设置自动清除不需要的import
- 下一篇: 关于JavaBean