讓咱們先從事務提及,「什麼是事務?咱們爲何須要事務?」。事務是一組沒法被分割的操做,要麼全部操做所有成功,要麼所有失敗。咱們在開發中須要經過事務將一些操做組成一個單元,來保證程序邏輯上的正確性,例如所有插入成功,或者回滾,一條都不插入。做爲程序員的咱們,對於事務管理,所須要作的即是進行事務的界定,即經過相似begin transaction和end transaction的操做來界定事務的開始和結束。html
下面是一個基本的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進行事務管理的代碼直觀上來看,存在兩個問題:spring
而若是咱們須要更換其餘數據訪問技術,例如Hibernate、MyBatis、JPA等,雖然事務管理的操做都相似,但API卻不一樣,則需使用相應的API來改寫。這也會引來第三個問題:數據庫
上文列出了三個待解決的問題,下面咱們看Spring事務是如何解決。編程
2.1 繁雜的事務管理APIapp
針對該問題,咱們很容易能夠想到,在衆多事務管理的API上抽象一層。經過定義接口屏蔽具體實現,再使用策略模式來決定具體的API。下面咱們看下Spring事務中定義的抽象接口。
在Spring事務中,核心接口是PlatformTransactionManager,也叫事務管理器,其定義以下:ide
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中會包含事務的元信息:線程
根據TransactionDefinition得到的TransactionStatus中會封裝事務對象,並提供了操做事務和查看事務狀態的方法,例如:debug
還支持嵌套事務的相關方法:代理
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"); } proce***ollback(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"); } proce***ollback(defStatus, true); return; } // 4.提交事務 processCommit(defStatus); }
在commit模板方法中定義了事務提交的基本邏輯,經過查看status的事務狀態來決定拋出異常仍是回滾,或是提交。其中的proce***ollback和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 { // 執行commit con.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提供聲明式事務將業務處理代碼和事務管理代碼分離。
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 { @Autowired Test 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節中咱們說到只有拋出非檢查異常或是rollbackFor中指定的異常才能觸發回滾。若是咱們把異常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