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 {
// 執行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提供聲明式事務將業務處理代碼和事務管理代碼分離。
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 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之道公衆號
好文章,我在看❤️