一个@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。
出現了自調用。什么是自調用呢?我們看個例子
上面三個方法都在同一個類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時調用的是目標類中的方法,這種清空下,事務當然會失效。
常見的自調用導致的事務失效還有一個例子,如下:
@Service public?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方法的前后并不會執行事務的相關操作。這也是自調用帶來問題的根本原因:「自調用時,調用的是目標類中的方法而不是代理類中的方法」
「解決方案」:
自己注入自己,然后顯示的調用,例如:
@Service public?class?DmzService?{//?自己注入自己@AutowiredDmzService?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,如下:
@Service public?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)?
個人比較喜歡的是第二種方式
這里我們做個來做個小總結
總結
一圖勝千言
事務失效的原因事務回滾相關問題
回滾相關的問題可以被總結為兩句話
想回滾的時候事務卻提交了
想提交的時候被標記成只能回滾了(rollback only)
先看第一種情況:「想回滾的時候事務卻提交了」。這種情況往往是程序員對Spring中事務的rollbackFor屬性不夠了解導致的。
?Spring默認拋出了未檢查unchecked異常(繼承自 RuntimeException 的異常)或者 Error才回滾事務;其他異常不會觸發回滾事務,已經執行的SQL會提交掉。如果在事務中拋出其他類型的異常,但卻期望 Spring 能夠回滾事務,就需要指定rollbackFor屬性。?
對應代碼其實我們上篇文章也分析過了,如下:
回滾代碼?以上代碼位于:TransactionAspectSupport#completeTransactionAfterThrowing方法中?
默認情況下,只有出現RuntimeException或者Error才會回滾
public?boolean?rollbackOn(Throwable?ex)?{return?(ex?instanceof?RuntimeException?||?ex?instanceof?Error); }所以,如果你想在出現了非RuntimeException或者Error時也回滾,請指定回滾時的異常,例如:
@Transactional(rollbackFor?=?Exception.class)第二種情況:「想提交的時候被標記成只能回滾了(rollback only)」。
對應的異常信息如下:
Transaction?rolled?back?because?it?has?been?marked?as?rollback-only我們先來看個例子吧
@Service public?class?DmzService?{@AutowiredIndexService?indexService;@Transactionalpublic?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方法都開啟了事務,并且事務的傳播級別為required,所以當我們在testRollbackOnly中調用IndexService的a方法時這兩個方法應當是共用的一個事務。按照這種思路,雖然IndexService的a方法拋出了異常,但是我們在testRollbackOnly將異常捕獲了,那么這個事務應該是可以正常提交的,為什么會拋出異常呢?
如果你看過我之前的源碼分析的文章應該知道,在處理回滾時有這么一段代碼
rollBackOnly設置在提交時又做了下面這個判斷(這個方法我刪掉了一些不重要的代碼)
commit_rollbackOnly可以看到當提交時發現事務已經被標記為rollbackOnly后會進入回滾處理中,并且unexpected傳入的為true。在處理回滾時又有下面這段代碼
拋出異常最后在這里拋出了這個異常。
?以上代碼均位于AbstractPlatformTransactionManager中?
總結起來,「主要的原因就是因為內部事務回滾時將整個大事務做了一個rollbackOnly的標記」,所以即使我們在外部事務中catch了拋出的異常,整個事務仍然無法正常提交,并且如果你希望正常提交,Spring還會拋出一個異常。
「解決方案」:
這個解決方案要依賴業務而定,你要明確你想要的結果是什么
內部事務發生異常,外部事務catch異常后,內部事務自行回滾,不影響外部事務
?將內部事務的傳播級別設置為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(); }?
雖然這兩者都能得到上面的結果,但是它們之間還是有不同的。當傳播級別為requires_new時,兩個事務完全沒有聯系,各自都有自己的事務管理機制(開啟事務、關閉事務、回滾事務)。但是傳播級別為nested時,實際上只存在一個事務,只是在調用a方法時設置了一個保存點,當a方法回滾時,實際上是回滾到保存點上,并且當外部事務提交時,內部事務才會提交,外部事務如果回滾,內部事務會跟著回滾。
內部事務發生異常時,外部事務catch異常后,內外兩個事務都回滾,但是方法不拋出異常
?
通過顯示的設置事務的狀態為RollbackOnly。這樣當提交事務時會進入下面這段代碼
顯示回滾最大的區別在于處理回滾時第二個參數傳入的是false,這意味著回滾是回滾是預期之中的,所以在處理完回滾后并不會拋出異常。
?
讀寫分離跟事務結合使用時的問題
讀寫分離一般有兩種實現方式
配置多數據源
依賴中間件,如MyCat
如果是配置了多數據源的方式實現了讀寫分離,那么需要注意的是:「如果開啟了一個讀寫事務,那么必須使用寫節點」,「如果是一個只讀事務,那么可以使用讀節點」
如果是依賴于MyCat等中間件那么需要注意:「只要開啟了事務,事務內的SQL都會使用寫節點(依賴于具體中間件的實現,也有可能會允許使用讀節點,具體策略需要自行跟DB團隊確認)」
基于上面的結論,我們在使用事務時應該更加謹慎,在沒有必要開啟事務時盡量不要開啟。
?一般我們會在配置文件配置某些約定的方法名字前綴開啟不同的事務(或者不開啟),但現在隨著注解事務的流行,好多開發人員(或者架構師)搭建框架的時候在service類上加上了@Transactional注解,導致整個類都是開啟事務的,這樣嚴重影響數據庫執行的效率,更重要的是開發人員不重視、或者不知道在查詢類的方法上面自己加上@Transactional(propagation=Propagation.NOT_SUPPORTED)就會導致,所有的查詢方法實際并沒有走從庫,導致主庫壓力過大。?
其次,關于如果沒有對只讀事務做優化的話(優化意味著將只讀事務路由到讀節點),那么@Transactional注解中的readOnly屬性就應該要慎用。我們使用readOnly的原本目的是為了將事務標記為只讀,這樣當MySQL服務端檢測到是一個只讀事務后就可以做優化,少分配一些資源(例如:只讀事務不需要回滾,所以不需要分配undo log段)。但是當配置了讀寫分離后,可能會可能會導致只讀事務內所有的SQL都被路由到了主庫,讀寫分離也就失去了意義。
總結
本文為事務專欄最后一篇啦!這篇文章主要是總結了工作中事務相關的常見問題,想讓大家少走點彎路!希望大家可以認真讀完哦,有什么問題可以直接在后臺私信我或者加我微信!
這篇文章也是整個Spring系列的最后一篇文章,之后可能會出一篇源碼閱讀心得,跟大家聊聊如何學習源碼。
另外今年也給自己定了個小目標,就是完成SSM框架源碼的閱讀。目前來說Spring是完成,接下來就是SpringMVC跟MyBatis。
在分析MyBatis前,會從JDBC源碼出發,然后就是MyBatis對配置的解析、MyBatis執行流程、MyBatis的緩存、MyBatis的事務管理以及MyBatis的插件機制。
在學習SpringMVC前,會從TomCat出發,先講清楚TomCat的原理,我們再來看SpringMVC。整個來說相比于Spring源碼,我覺得應該不算特別難。
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的一个@Transaction哪里来这么多坑?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 问题 D: 约数的个数
- 下一篇: 为什么阿里巴巴不允许使用Executor