在上一篇博文《Spring事務原理 一》中,我們熟悉了Spring聲明式事務的AOP原理,以及事務執行的大體流程。
本文中,介紹了Spring事務的核心組件、傳播行為的源碼實現。下一篇中,我們將結合案例,來講解實戰中有關事務的易錯點。
本文中源碼來自Spring 5.3.x分支,github源碼地址:GitHub - spring-projects/spring-framework: Spring Framework
一 Spring事務的核心組件
了解相關類和接口,看看Spring對概念、術語是如何封裝的?
1.1 PlatformTransactionManager
事務管理器接口,負責獲取數據庫連接,事務的開啟、提交和回滾。
有抽象實現AbstractPlatformTransactionManager,其中定義了doGetTransaction、doBegin、doCommit、doRollback等抽象方法,待子類實現。
AbstractPlatformTransactionManager中有幾個值得關注的屬性:
// 是否允許嵌套事務
private boolean nestedTransactionAllowed = false;// 局部失敗時全局回滾,當為false時,部分失敗則不回滾
private boolean globalRollbackOnParticipationFailure = true;private boolean failEarlyOnGlobalRollbackOnly = false;private boolean rollbackOnCommitFailure = false;
它有以下常見子類:
DataSourceTransactionManager
:用于JDBC和MyBatis等基于數據源的事務管理。HibernateTransactionManager
:用于Hibernate框架的事務管理。JpaTransactionManager
:用于JPA(Java Persistence API)的事務管理。
DataSourceTransactionManager
該類中定義了兩個屬性,對doCommit等抽象方法提供實現。
- doGetTransaction方法:創建一個DataSourceTransactionObject對象,設置connection。
- doBegin方法:執行事務前的準備工作,如設置
- 如果DataSourceTransactionObject沒有連接,則獲取一個連接
- 根據TransactionDefinition,為connection設置屬性,如isolationLevel、readOnly、timeout;
- connection.setAutoCommit(false),關閉自動提交;
-
- 將connection與當前線程綁定;
-
- doCommit方法:從TransactionStatus中獲取TransactionObject,拿到connection調用commit();
- doRollback方法:與doCommit實現相似,只是調connection.rollback();
1.2 TransactionDefinition
定義事務的屬性,如隔離級別、傳播行為、超時時間等。
隔離級別(Isolation Level):定義了事務之間的隔離程度,常見的有:
DEFAULT
:使用數據庫默認的隔離級別。READ_UNCOMMITTED
:允許讀取未提交的數據,可能導致臟讀。READ_COMMITTED
:只能讀取已提交的數據,避免臟讀。REPEATABLE_READ
:確保在同一事務中多次讀取同一數據時,結果一致。SERIALIZABLE
:最高的隔離級別,確保事務串行執行,避免臟讀、不可重復讀和幻讀。
超時時間(Timeout):事務的超時時間,超過該時間未完成則自動回滾。
只讀(Read-only):指定事務是否為只讀事務,優化性能。
在子類DefaultTransactionDefinition中,可以看到默認值:PROPAGATION_REQUIRED、ISOLATION_DEFAULT、TIMEOUT_DEFAULT、非readOnly。
當使用@Transactional時,會將注解屬性解析成一個TransactionDefinition對象。
1.3 TransactionStatus
表示事務的狀態,提供了以下方法:
isNewTransaction()
:判斷當前事務是否為新事務。hasSavepoint()
:判斷是否存在保存點(用于嵌套事務)。setRollbackOnly()
:標記事務為回滾狀態。isRollbackOnly()
:判斷事務是否被標記為回滾。
在子類DefaultTransactionStatus中,有這些屬性
private boolean rollbackOnly = false;private boolean completed = false;private final Object transaction;private final boolean newTransaction;private final boolean newSynchronization;private final boolean readOnly;
1.4 TransactionSynchronizationManager
事務同步管理器,用于將事務相關信息與當前線程綁定,以支持各種事務傳播行為。其中有多個ThreadLocal屬性。
為什么保存連接的resources是Map類型?因為支持多數據源,當一個方法中操作多個數據庫時,線程中就得保存多個connectionHolder對象,因此使用Map結構,key就是dataSource對象。
1.5 TransactionInterceptor
事務攔截器,就是AOP的代理邏輯,具體實現在TransactionAspectSupport#invokeWithinTransaction中。
大體流程為:
- 獲取當前方法的@Transaction注解屬性,創建TransactionDefinition對象;
- 獲取TransactionManager對象;
- 根據方法名生成事務名;
- 如有必要則創建事務,并處理傳播行為;
- 在try中執行下一個interceptor或被代理對象中方法;
- 異常時先回滾事務,正常時提交事務;
- 當前方法執行結束,還原TransactionInfo(恢復上層方法的事務信息)。
二 事務的傳播機制
2.1 什么是事務傳播
在日常開發中,業務代碼中經常出現方法間調用,比如購物時下單減和庫存:
import com.xiakexing.dao.InventoryDao;
import com.xiakexing.dao.OrderDao;
import com.xiakexing.entity.Order;public class OrderService {private OrderDao orderDao;private InventoryDao inventoryDao;public void saveOrder(Order order) {orderDao.save(order);updateInventory(order.getCode(), order.getCount());}public void updateInventory(String code, int count) {inventoryDao.update(code, count);}
}
- saveOrder()和updateInventory(),所有sql需要在同一個事務中;
- 單獨調用updateInventory()時,如進貨時不需要事務。
可見,updateInventory方法,在不同場景下對事務有不同要求。Spring中又如何實現呢?
Spring定義了傳播行為(Propagation Behavior),定義了方法間調用時事務如何傳遞,類型有:
REQUIRED
:如果當前線程存在事務,則加入該事務;如果不存在,則創建一個新事務。REQUIRES_NEW
:總是創建一個新事務,如果當前線程存在事務,則掛起當前事務。SUPPORTS
:如果當前線程存在事務,則加入該事務;如果不存在,則以非事務方式執行。NOT_SUPPORTED
:以非事務方式執行,如果當前線程存在事務,則掛起當前事務。MANDATORY
:如果當前線程存在事務,則加入該事務;如果不存在則拋出異常。NEVER
:以非事務方式執行,如果當前線程存在事務,則拋出異常。NESTED
:如果當前線程存在事務,則以嵌套事務中執行;如果不存在,則創建一個新事務。
這兒為什么強調線程呢?
方法間調用都在某個線程的方法棧中,按FILO順序執行。如果兩個方法的中sql使用兩個不同的數據庫連接執行,顯然無法納入一個事務中。
因為,數據庫連接必須能夠跨方法傳遞,Spring底層就是將connection放到ThreadLocal中。
2.2 傳播機制的實現
2.2.1 線程綁定連接
在DataSourceTransactionManager#doBegin中:
- 從DataSource獲取數據連接connection,設置autocommit=false、隔離級別、超時時間等屬性;
- 將connection放入ThreadLocal<Map>,Map的key是DataSource對象,value是connectionHolder對象。
可見,方法間調用時可以從ThreadLocal中拿到同一個連接,去執行不同的SQL,進而一同提交或回滾。
2.2.2 處理傳播機制
真正執行被代理對象方法前,會判斷是否創建事務。
調用AbstractPlatformTransactionManager#getTransaction,邏輯如下:
- 創建DataSourceTransactionObject對象,從ThreadLocal中獲取connectionHolder(可能為null);
- 當connectionHolder不為null且connectionHolder.transactionActive=ture時,說明已存在事務:
如果當前線程中存在事務:
- 如果當前方法傳播行為是PROPAGATION_NEVER,則拋異常
-
- 如果是PROPAGATION_NOT_SUPPORTED,則掛起當前事務,用一個新連接的非事務方式執行當前方法;
-
- 如果是PROPAGATION_REQUIRES_NEW,則掛起當前事務,開啟一個新事務(獲取新連接并帶事務執行);
-
- 如果是PROPAGATION_NESTED,則先設置savepoints(可以回滾到此處),然后使用同一個連接繼續執行。
-
如果當前線程中不存在事務:
- 如果傳播行為是PROPAGATION_MANDATORY,則拋異常
-
- 如果傳播行為是PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED,則開啟事務
-
流程圖如下:
三 總結
- Spring中,對于事務這一抽象概念,從多個方法進行了良好封裝,如將隔離級別、超時時間等封裝為TransactionDefinition,將事務狀態、是否回滾等封裝為TransactionStatus。
- 事務的傳播行為,發生在方法間調用中。通過將connectionHolder放入ThreadLocal,實現了不同方法中使用同一數據庫連接,從而支持多種傳播方式。
- 事務底層,就是通過設置connection.autocommit為false,從而根據方法是否異常,選擇commit還是rollback;
- 通過Savepoint實現嵌套事務(需要數據庫支持)。
- 在執行某個方法時,判斷當前是否已經存在事務,就是判斷當前線程的ThreadLocal中是否存在一個數據庫連接對象。