天天看點

三問Spring事務:解決什麼問題?如何解決?存在什麼問題?

三問Spring事務:解決什麼問題?如何解決?存在什麼問題?
三問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進行事務管理的代碼直覺上來看,存在兩個問題:

  1. 業務處理代碼與事務管理代碼混雜;
  2. 大量的異常處理代碼(在catch中還要try-catch)。

而如果我們需要更換其他資料通路技術,例如Hibernate、MyBatis、JPA等,雖然事務管理的操作都類似,但API卻不同,則需使用相應的API來改寫。這也會引來第三個問題:

  1. 繁雜的事務管理API。

上文列出了三個待解決的問題,下面我們看Spring事務是如何解決。

三問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 {
        // 執行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事務:解決什麼問題?如何解決?存在什麼問題?

聲明式事務原理

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. 存在什麼問題

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事務:解決什麼問題?如何解決?存在什麼問題?

參考

  1. Spring Framework Documentation——Data Access: https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html
  2. 《Spring揭秘》
  3. 5-common-spring-transactional-pitfalls: https://codete.com/blog/5-common-spring-transactional-pitfalls/
  4. Spring事務原理一探: https://zhuanlan.zhihu.com/p/54067384