javascript
三问Spring事务:解决什么问题?如何解决?存在什么问题?
1. 解決什么問題
讓我們先從事務說起,“什么是事務?我們為什么需要事務?”。事務是一組無法被分割的操作,要么所有操作全部成功,要么全部失敗。我們在開發中需要通過事務將一些操作組成一個單元,來保證程序邏輯上的正確性,例如全部插入成功,或者回滾,一條都不插入。作為程序員的我們,對于事務管理,所需要做的便是進行事務的界定,即通過類似begin transaction和end transaction的操作來界定事務的開始和結束。
下面是一個基本的JDBC事務管理代碼:
//?開啟數據庫連接 Connection?con?=?openConnection(); try?{//?關閉自動提交con.setAutoCommit(false);//?業務處理//?...??//?提交事務con.commit(); }?catch?(SQLException?|?MyException?e)?{//?捕獲異常,回滾事務try?{con.rollback();}?catch?(SQLException?ex)?{ex.printStackTrace();} }?finally?{//?關閉連接try?{con.setAutoCommit(true);con.close();}?catch?(SQLException?e)?{e.printStackTrace();} }直接使用JDBC進行事務管理的代碼直觀上來看,存在兩個問題:
業務處理代碼與事務管理代碼混雜;
大量的異常處理代碼(在catch中還要try-catch)。
而如果我們需要更換其他數據訪問技術,例如Hibernate、MyBatis、JPA等,雖然事務管理的操作都類似,但API卻不同,則需使用相應的API來改寫。這也會引來第三個問題:
繁雜的事務管理API。
上文列出了三個待解決的問題,下面我們看Spring事務是如何解決。
??
2. 如何解決
2.1 繁雜的事務管理API
針對該問題,我們很容易可以想到,在眾多事務管理的API上抽象一層。通過定義接口屏蔽具體實現,再使用策略模式來決定具體的API。下面我們看下Spring事務中定義的抽象接口。
在Spring事務中,核心接口是PlatformTransactionManager,也叫事務管理器,其定義如下:
public?interface?PlatformTransactionManager?extends?TransactionManager?{//?獲取事務(新的事務或者已經存在的事務)TransactionStatus?getTransaction(@Nullable?TransactionDefinition?definition)throws?TransactionException;???//?提交事務void?commit(TransactionStatus?status)?throws?TransactionException;//?回滾事務void?rollback(TransactionStatus?status)?throws?TransactionException; }getTransaction通過入參TransactionDefinition來獲得TransactionStatus,即通過定義的事務元信息來創建相應的事務對象。在TransactionDefinition中會包含事務的元信息:
PropagationBehavior:傳播行為;
IsolationLevel:隔離級別;
Timeout:超時時間;
ReadOnly:是否只讀。
根據TransactionDefinition獲得的TransactionStatus中會封裝事務對象,并提供了操作事務和查看事務狀態的方法,例如:
setRollbackOnly:標記事務為Rollback-only,以使其回滾;
isRollbackOnly:查看是否被標記為Rollback-only;
isCompleted:查看事務是否已完成(提交或回滾完成)。
還支持嵌套事務的相關方法:
createSavepoint:創建savepoint;
rollbackToSavepoint:回滾到指定savepoint;
releaseSavePoint:釋放savepoint。
TransactionStatus事務對象可被傳入到commit方法或rollback方法中,完成事務的提交或回滾。
下面我們通過一個具體實現來理解TransactionStatus的作用。以commit方法為例,如何通過TransactionStatus完成事務的提交。AbstractPlatformTransactionManager是PlatformTransactionManager接口的的實現,作為模板類,其commit實現如下:
public?final?void?commit(TransactionStatus?status)?throws?TransactionException?{//?1.檢查事務是否已完成if?(status.isCompleted())?{throw?new?IllegalTransactionStateException("Transaction?is?already?completed?-?do?not?call?commit?or?rollback?more?than?once?per?transaction");}//?2.檢查事務是否需要回滾(局部事務回滾)DefaultTransactionStatus?defStatus?=?(DefaultTransactionStatus)?status;if?(defStatus.isLocalRollbackOnly())?{if?(defStatus.isDebug())?{logger.debug("Transactional?code?has?requested?rollback");}processRollback(defStatus,?false);return;}//?3.檢查事務是否需要回滾(全局事務回滾)if?(!shouldCommitOnGlobalRollbackOnly()?&&?defStatus.isGlobalRollbackOnly())?{if?(defStatus.isDebug())?{logger.debug("Global?transaction?is?marked?as?rollback-only?but?transactional?code?requested?commit");}processRollback(defStatus,?true);return;}//?4.提交事務processCommit(defStatus); }在commit模板方法中定義了事務提交的基本邏輯,通過查看status的事務狀態來決定拋出異常還是回滾,或是提交。其中的processRollback和processCommit方法也是模板方法,進一步定義了回滾、提交的邏輯。以processCommit方法為例,具體的提交操作將由抽象方法doCommit完成。
protected?abstract?void?doCommit(DefaultTransactionStatus?status)?throws?TransactionException;doCommit的實現取決于具體的數據訪問技術。我們看下JDBC相應的具體實現類DataSourceTransactionManager中的doCommit實現。
protected?void?doCommit(DefaultTransactionStatus?status)?{//?獲取status中的事務對象????DataSourceTransactionObject?txObject?=?(DataSourceTransactionObject)?status.getTransaction();//?通過事務對象獲得數據庫連接對象Connection?con?=?txObject.getConnectionHolder().getConnection();if?(status.isDebug())?{logger.debug("Committing?JDBC?transaction?on?Connection?["?+?con?+?"]");}try?{//?執行commitcon.commit();}catch?(SQLException?ex)?{throw?new?TransactionSystemException("Could?not?commit?JDBC?transaction",?ex);} }在commit和processCommit方法中我們根據入參的TransactionStatus提供的事務狀態來決定事務行為,而在doCommit中需要執行事務提交時將會通過TransactionStatus中的事務對象來獲得數據庫連接對象,再執行最后的commit操作。通過這個示例我們可以理解TransactionStatus所提供的事務狀態和事務對象的作用。
下面是用Spring事務API改寫后的事務管理代碼:
//?獲得事務管理器 PlatformTransactionManager?txManager?=?getPlatformTransactionManager(); DefaultTransactionDefinition?def?=?new?DefaultTransactionDefinition(); //?指定事務元信息 def.setName("SomeTxName"); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); //?獲得事務 TransactionStatus?status?=?txManager.getTransaction(def); try?{//?業務處理 } catch?(MyException?ex)?{//?捕獲異常,回滾事務txManager.rollback(status);throw?ex; } //?提交事務 txManager.commit(status);無論是使用JDBC、Hibernate還是MyBatis,我們只需要傳給txManager相應的具體實現就可以在多種數據訪問技術中切換。
小結:Spring事務通過PlatformTransactionManager、TransactionDefinition和TransactionStatus接口統一事務管理API,并結合策略模式和模板方法決定具體實現。
Spring事務API代碼還有個特點有沒有發現,SQLException不見了。下面來看Spring事務是如何解決大量的異常處理代碼。
2.2 大量的異常處理代碼
為什么使用JDBC的代碼中會需要寫這么多的異常處理代碼。這是因為Connection的每個方法都會拋出SQLException,而SQLException又是檢查異常,這就強制我們在使用其方法時必須進行異常處理。那Spring事務是如何解決該問題的。我們看下doCommit方法:
protected?void?doCommit(DefaultTransactionStatus?status)?{DataSourceTransactionObject?txObject?=?(DataSourceTransactionObject)?status.getTransaction();Connection?con?=?txObject.getConnectionHolder().getConnection();if?(status.isDebug())?{logger.debug("Committing?JDBC?transaction?on?Connection?["?+?con?+?"]");}try?{con.commit();}catch?(SQLException?ex)?{//?異常轉換throw?new?TransactionSystemException("Could?not?commit?JDBC?transaction",?ex);} }Connection的commit方法會拋出檢查異常SQLException,在catch代碼塊中SQLException將被轉換成TransactionSystemException拋出,而TransactionSystemException是一個非檢查異常。通過將檢查異常轉換成非檢查異常,讓我們能夠自行決定是否捕獲異常,不強制進行異常處理。
Spring事務中幾乎為數據庫的所有錯誤都定義了相應的異常,統一了JDBC、Hibernate、MyBatis等不同異常API。這有助于我們在處理異常時使用統一的異常API接口,無需關心具體的數據訪問技術。
小結:Spring事務通過異常轉換避免強制異常處理。
2.3 業務處理代碼與事務管理代碼混雜
在2.1節中給出了使用Spring事務API的寫法,即編程式事務管理,但仍未解決“業務處理代碼與事務管理代碼混雜”的問題。這時候就可以利用Spring AOP將事務管理代碼這一橫切關注點從代碼中剝離出來,即聲明式事務管理。以注解方式為例,通過為方法標注@Transaction注解,將為該方法提供事務管理。其原理如下圖所示:
聲明式事務原理
Spring事務會為@Transaction標注的方法的類生成AOP增強的動態代理類對象,并且在調用目標方法的攔截鏈中加入TransactionInterceptor進行環繞增加,實現事務管理。
下面我們看下TransactionInterceptor中的具體實現,其invoke方法中將調用invokeWithinTransaction方法進行事務管理,如下所示:
protected?Object?invokeWithinTransaction(Method?method,?Class<?>?targetClass,?final?InvocationCallback?invocation)throws?Throwable?{//?查詢目標方法事務屬性、確定事務管理器、構造連接點標識(用于確認事務名稱)final?TransactionAttribute?txAttr?=?getTransactionAttributeSource().getTransactionAttribute(method,?targetClass);final?PlatformTransactionManager?tm?=?determineTransactionManager(txAttr);final?String?joinpointIdentification?=?methodIdentification(method,?targetClass,?txAttr);if?(txAttr?==?null?||?!(tm?instanceof?CallbackPreferringPlatformTransactionManager))?{//?創建事務TransactionInfo?txInfo?=?createTransactionIfNecessary(tm,?txAttr,?joinpointIdentification);Object?retVal?=?null;try?{//?通過回調執行目標方法retVal?=?invocation.proceedWithInvocation();}catch?(Throwable?ex)?{//?目標方法執行拋出異常,根據異常類型執行事務提交或者回滾操作completeTransactionAfterThrowing(txInfo,?ex);throw?ex;}finally?{//?清理當前線程事務信息cleanupTransactionInfo(txInfo);}//?目標方法執行成功,提交事務commitTransactionAfterReturning(txInfo);return?retVal;}?else?{//?帶回調的事務執行處理,一般用于編程式事務//?...} }在調用目標方法前后加入了創建事務、處理異常、提交事務等操作。這讓我們不必編寫事務管理代碼,只需通過@Transaction的屬性指定事務相關元信息。
小結:Spring事務通過AOP提供聲明式事務將業務處理代碼和事務管理代碼分離。
?
3. 存在什么問題
Spring事務為了我們解決了第一節中列出的三個問題,但同時也會帶來些新的問題。
3.1 非public方法失效
@Transactional只有標注在public級別的方法上才能生效,對于非public方法將不會生效。這是由于Spring AOP不支持對private、protect方法進行攔截。從原理上來說,動態代理是通過接口實現,所以自然不能支持private和protect方法的。而CGLIB是通過繼承實現,其實是可以支持protect方法的攔截的,但Spring AOP中并不支持這樣使用,筆者猜測做此限制是出于代理方法應是public的考慮,以及為了保持CGLIB和動態代理的一致。如果需要對protect或private方法攔截則建議使用AspectJ。
3.2 自調用失效
當通過在Bean的內部方法直接調用帶有@Transactional的方法時,@Transactional將失效,例如:
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(b); }在saveAB中調用saveA和saveB方法,兩者的@Transactional都將失效。這是因為Spring事務的實現基于代理類,當在內部直接調用方法時,將不會經過代理對象,而是直接調用目標對象的方法,無法被TransactionInterceptor攔截處理。解決辦法:
(1)ApplicationContextAware
通過ApplicationContextAware注入的上下文獲得代理對象。
public?void?saveAB(A?a,?B?b) {Test?self?=?(Test)?applicationContext.getBean("Test");self.saveA(a);self.saveB(b); }(2)AopContext
通過AopContext獲得代理對象。
public?void?saveAB(A?a,?B?b) {Test?self?=?(Test)AopContext.currentProxy();self.saveA(a);self.saveB(b); }(3)@Autowired
通過@Autowired注解注入代理對象。
@Component public?class?Test?{@AutowiredTest?self;public?void?saveAB(A?a,?B?b){self.saveA(a);self.saveB(b);}//?... }(4)拆分
將saveA、saveB方法拆分到另一個類中。
public?void?saveAB(A?a,?B?b) {txOperate.saveA(a);txOperate.saveB(b); }上述兩個問題都是由于Spring事務的實現方式的限制導致的問題。下面再看兩個由于使用不當容易犯錯的兩個問題。
3.3 檢查異常默認不回滾
在默認情況下,拋出非檢查異常會觸發回滾,而檢查異常不會。
根據invokeWithinTransaction方法,我們可以知道異常處理邏輯在completeTransactionAfterThrowing方法中,其實現如下:
protected?void?completeTransactionAfterThrowing(@Nullable?TransactionInfo?txInfo,?Throwable?ex)?{if?(txInfo?!=?null?&&?txInfo.getTransactionStatus()?!=?null)?{if?(logger.isTraceEnabled())?{logger.trace("Completing?transaction?for?["?+?txInfo.getJoinpointIdentification()?+"]?after?exception:?"?+?ex);}if?(txInfo.transactionAttribute?!=?null?&&?txInfo.transactionAttribute.rollbackOn(ex))?{try?{//?異常類型為回滾異常,執行事務回滾txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());}catch?(TransactionSystemException?ex2)?{logger.error("Application?exception?overridden?by?rollback?exception",?ex);ex2.initApplicationException(ex);throw?ex2;}catch?(RuntimeException?|?Error?ex2)?{logger.error("Application?exception?overridden?by?rollback?exception",?ex);throw?ex2;}}else?{try?{//?異常類型為非回滾異常,仍然執行事務提交txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());}catch?(TransactionSystemException?ex2)?{logger.error("Application?exception?overridden?by?commit?exception",?ex);ex2.initApplicationException(ex);throw?ex2;}catch?(RuntimeException?|?Error?ex2)?{logger.error("Application?exception?overridden?by?commit?exception",?ex);throw?ex2;}}} }根據rollbackOn判斷異常是否為回滾異常。只有RuntimeException和Error的實例,即非檢查異常,或者在@Transaction中通過rollbackFor屬性指定的回滾異常類型,才會回滾事務。否則將繼續提交事務。所以如果需要對非檢查異常進行回滾,需要記得指定rollbackFor屬性,不然將回滾失效。
3.4 catch異常無法回滾
在3.3節中我們說到只有拋出非檢查異?;蚴莚ollbackFor中指定的異常才能觸發回滾。如果我們把異常catch住,而且沒拋出,則會導致無法觸發回滾,這也是開發中常犯的錯誤。例如:
@Transactional public?void?insert(List<User>?users)?{try?{JdbcTemplate?jdbcTemplate?=?new?JdbcTemplate(dataSource);for?(User?user?:?users)?{String?insertUserSql?=?"insert?into?User?(id,?name)?values?(?,?)";jdbcTemplate.update(insertUserSql,?new?Object[]?{?user.getId(),user.getName()?});}}?catch?(Exception?e)?{e.printStackTrace();} }這里由于catch住了所有Exception,并且沒拋出。當插入發生異常時,將不會觸發回滾。
但同時我們也可以利用這種機制,用try-catch包裹不用參與事務的數據操作,例如對于寫入一些不重要的日志,我們可將其用try-catch包裹,避免拋出異常,則能避免寫日志失敗而影響事務的提交。
參考
Spring Framework Documentation——Data Access:?https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html
《Spring揭秘》
5-common-spring-transactional-pitfalls:?https://codete.com/blog/5-common-spring-transactional-pitfalls/
Spring事務原理一探:?https://zhuanlan.zhihu.com/p/54067384
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的三问Spring事务:解决什么问题?如何解决?存在什么问题?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NYOJ 642 牛奶
- 下一篇: 技术转管理,用什么来拯救自己?