一文讀懂Spring事務管理器

爲什麼需要事務管理器

如果沒有事務管理器的話,我們的程序可能是這樣:

Connection connection = acquireConnection();
try{
    int updated = connection.prepareStatement().executeUpdate();
    connection.commit();
}catch (Exception e){
    rollback(connection);
}finally {
    releaseConnection(connection);
}

也有可能是這樣"優雅的事務":

execute(new TxCallback() {
    @Override
    public Object doInTx(Connection var1) {
        //do something...
        return null;
    }
});
public void execute(TxCallback txCallback){
    Connection connection = acquireConnection();
    try{
        txCallback.doInTx(connection);
        connection.commit();
    }catch (Exception e){
        rollback(connection);
    }finally {
        releaseConnection(connection);
    }
}

# lambda版
execute(connection -> {
    //do something...
    return null;
});

但是以上兩種方式,針對一些複雜的場景是很不方便的。在實際的業務場景中,往往有比較複雜的業務邏輯,代碼冗長,邏輯關聯複雜,如果一個大操作中有全是這種代碼的話我想開發人員可能會瘋把。更不用提定製化的隔離級別,以及嵌套/獨立事務的處理了。

Spring 事務管理器簡介

Spring作爲Java最強框架,事務管理也是其核心功能之一。Spring爲事務管理提供了統一的抽象,有以下優點:

  • 跨不同事務API(例如Java事務API(JTA),JDBC,Hibernate,Java持久性API(JPA)和Java數據對象(JDO))的一致編程模型。
  • 支持聲明式事務管理(註解形式)
  • 與JTA之類的複雜事務API相比, 用於程序化事務管理的API更簡單
  • 和Spring的Data層抽象集成方便(比如Spring - Hibernate/Jdbc/Mybatis/Jpa...)

使用方式

事務,自然是控制業務的,在一個業務流程內,往往希望保證原子性,要麼全成功要麼全失敗。

所以事務一般是加載@Service層,一個Service內調用了多個操作數據庫的操作(比如Dao),在Service結束後事務自動提交,如有異常拋出則事務回滾。

這也是Spring事務管理的基本使用原則。

下面貼出具體的使用代碼:

註解

在被Spring管理的類頭上增加@Transactional註解,即可對該類下的所有方法開啓事務管理。事務開啓後,方法內的操作無需手動開啓/提交/回滾事務,一切交給Spring管理即可。

@Service
@Transactional
public class TxTestService{
    
    @Autowired
    private OrderRepo orderRepo;

    public void submit(Order order){
        orderRepo.save(order);
    }
}

也可以只在方法上配置,方法配置的優先級是大於類的

@Service
public class TxTestService{
    
    @Autowired
    private OrderRepo orderRepo;


    @Transactional
    public void submit(Order order){
        orderRepo.save(order);
    }
}

XML

XML的配置方式較爲古老,此處就不貼代碼了,如有需要自行搜索

隔離級別

事務隔離級別是數據庫最重要的特性之一,他保證了髒讀/幻讀等問題不會發生。作爲一個事務管理框架自然也是支持此配置的,在@Transactional註解中有一個isolation配置,可以很方便的配置各個事務的隔離級別,等同於connection.setTransactionIsolation()

Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}

傳播行爲

可能沒有接觸過Spring的人聽到傳播行爲會奇怪,這是個什麼東西。

其實這個傳播行爲和數據庫功能無關,只是事務管理器爲了處理複雜業務而設計的一個機制。

比如現在有這樣一個調用場景,A Service -> B Service -> C Service,但是希望A/B在一個事務內,C是一個獨立的事務,同時C如果出錯,不影響AB所在的事務。

此時,就可以通過傳播行爲來處理;將C Service的事務配置爲@Transactional(propagation = Propagation.REQUIRES_NEW)即可

Spring支持以下幾種傳播行爲:

REQUIRED

默認策略,優先使用當前事務(及當前線程綁定的事務資源),如果不存在事務,則開啓新事務

SUPPORTS

優先使用當前的事務(及當前線程綁定的事務資源),如果不存在事務,則以無事務方式運行

MANDATORY

優先使用當前的事務,如果不存在事務,則拋出異常

REQUIRES_NEW

創建一個新事務,如果存在當前事務,則掛起(Suspend)

NOT_SUPPORTED

以非事務方式執行,如果當前事務存在,則掛起當前事務。

NEVER

以非事務方式執行,如果當前事務存在,則拋出異常

回滾策略

@Transactional中有4個配置回滾策略的屬性,分爲Rollback策略,和NoRollback策略

默認情況下,RuntimeException和Error這兩種異常會導致事務回滾,普通的Exception(需要Catch的)異常不會回滾。

Rollback

配置需要回滾的異常類

# 異常類Class
Class<? extends Throwable>[] rollbackFor() default {};
# 異常類ClassName,可以是FullName/SimpleName
String[] rollbackForClassName() default {};

NoRollback

針對一些要特殊處理的業務邏輯,比如插一些日誌表,或者不重要的業務流程,希望就算出錯也不影響事務的提交。

可以通過配置NoRollbackFor來實現,讓某些異常不影響事務的狀態。

# 異常類Class
Class<? extends Throwable>[] noRollbackFor() default {};
# 異常類ClassName,可以是FullName/SimpleName
String[] noRollbackForClassName() default {};

只讀控制

設置當時事務的只讀標示,等同於connection.setReadOnly()

常見問題

事務沒生效

有下列代碼,入口爲test方法,在testTx方法中配置了@Transactional註解,同時在插入數據後拋出RuntimeException異常,但是方法執行後插入的數據並沒有回滾,竟然插入成功了

public void test(){
    testTx();
}

@Transactional
public void testTx(){
    UrlMappingEntity urlMappingEntity = new UrlMappingEntity();
    urlMappingEntity.setUrl("http://www.baidu.com");
    urlMappingEntity.setExpireIn(777l);
    urlMappingEntity.setCreateTime(new Date());
    urlMappingRepository.save(urlMappingEntity);
    if(true){
        throw new RuntimeException();
    }
}

這裏不生效的原因是因爲入口的方法/類沒有增加@Transaction註解,由於Spring的事務管理器也是基於AOP實現的,不管是Cglib(ASM)還是Jdk的動態代理,本質上也都是子類機制;在同類之間的方法調用會直接調用本類代碼,不會執行動態代理曾的代碼;所以在這個例子中,由於入口方法test沒有增加代理註解,所以textTx方法上增加的事務註解並不會生效

異步後事務失效

比如在一個事務方法中,開啓了子線程操作庫,那麼此時子線程的事務和主線程事務是不同的。

因爲在Spring的事務管理器中,事務相關的資源(連接,session,事務狀態之類)都是存放在TransactionSynchronizationManager中的,通過ThreadLocal存放,如果跨線程的話就無法保證一個事務了

# TransactionSynchronizationManager.java
private static final ThreadLocal<Map<Object, Object>> resources =
        new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
        new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
        new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
        new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
        new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
        new NamedThreadLocal<>("Actual transaction active");

事務提交失敗

org.springframework.transaction.UnexpectedRollbackException: 
Transaction silently rolled back because it has been marked as rollback-only

這個異常是由於在同一個事務內,多個事務方法之間調用,子方法拋出異常,但又被父方法忽略了導致的。

因爲子方法拋出了異常,Spring事務管理器會將當前事務標爲失敗狀態,準備進行回滾,可是當子方法執行完畢出棧後,父方法又忽略了此異常,待方法執行完畢後正常提交時,事務管理器會檢查回滾狀態,若有回滾標示則拋出此異常。具體可以參考org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit

示例代碼:

A -> B
# A Service(@Transactional):
public void testTx(){
    urlMappingRepo.deleteById(98l);
    try{
        txSubService.testSubTx();
    }catch (Exception e){
        e.printStackTrace();
    }
}

# B Service(@Transactional)
public void testSubTx(){
    if(true){
        throw new RuntimeException();
    }
}

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章