咱們從頭到尾說一次 Spring 事務管理(器)
事務管理,一個被說爛的也被看爛的話題,還是八股文中的基礎股之一。但除了八股文中需要熟讀并背誦的那些個傳播行為之外,背后的“為什么”和核心原理更為重要。
寫這篇文章之前,我也翻過一些事務管理器原理介紹文章,但大多都是生硬的翻譯源碼,一個勁的給源碼加注釋。這種源碼翻譯的文章雖說有幫助,但對讀者來說體驗并不好,很容易陷入代碼的一些細節(jié)里,并不能幫助讀者快速的了解事務管理的全貌,以及設計思路。
本文會從設計角度,一步步的剖析 Spring 事務管理的設計思路(都會設計事務管理器了,還能不不會用么?)
為什么需要事務管理?
先看看如果沒有事務管理器的話,如果想讓多個操作(方法/類)處在一個事務里應該怎么做:
// MethodA:
public void methodA(){
Connection connection = acquireConnection();
try{
int updated = connection.prepareStatement().executeUpdate();
methodB(connection);
connection.commit();
}catch (Exception e){
rollback(connection);
}finally {
releaseConnection(connection);
}
}
// MethodB:
public void methodB(Connection connection){
int updated = connection.prepareStatement().executeUpdate();
}
復制代碼或者用 ThreadLocal 存儲 Connection?
static ThreadLocal<Connection> connHolder = new ThreadLocal<>();
// MethodA:
public void methodA(){
Connection connection = acquireConnection();
connHolder.set(connection);
try{
int updated = connection.prepareStatement().executeUpdate();
methodB();
connection.commit();
}catch (Exception e){
rollback(connection);
}finally {
releaseConnection(connection);
connHolder.remove();
}
}
// MethodB:
public void methodB(){
Connection connection = connHolder.get();
int updated = connection.prepareStatement().executeUpdate();
}
復制代碼還是有點惡心,再抽象一下?將綁定 Connection 的操作提取為公共方法:
static ThreadLocal<Connection> connHolder = new ThreadLocal<>();
private void bindConnection(){
Connection connection = acquireConnection();
connHolder.set(connection);
}
private void unbindConnection(){
releaseConnection(connection);
connHolder.remove();
}
// MethodA:
public void methodA(){
try{
bindConnection();
int updated = connection.prepareStatement().executeUpdate();
methoB();
connection.commit();
}catch (Exception e){
rollback(connection);
}finally {
unbindConnection();
}
}
// MethodB:
public void methodB(){
Connection connection = connHolder.get();
int updated = connection.prepareStatement().executeUpdate();
}
復制代碼現(xiàn)在看起來好點了,不過我有一個新的需求:想讓 methodB 獨立一個新事務,單獨提交和回滾,不影響 methodA
這……可就有點難搞了,ThreadLocal 中已經(jīng)綁定了一個 Connection,再新事務的話就不好辦了
那如果再復雜點呢,methodB 中需要調(diào)用 methodC,methodC 也需要一個獨立事務……
而且,每次 bind/unbind 的操作也有點太傻了,萬一哪個方法忘了寫 unbind ,最后來一個連接泄露那不是完蛋了!
好在 Spring 提供了事務管理器,幫我們解決了這一系列痛點。
Spring 事務管理解決了什么問題?
Spring 提供的事務管理可以幫我們管理事務相關的資源,比如 JDBC 的 Connection、Hibernate 的 Session、Mybatis 的 SqlSession。如說上面的 Connection 綁定到 ThreadLocal 來解決共享一個事務的這種方式,Spring 事務管理就已經(jīng)幫我們做好了。
還可以幫我們處理復雜場景下的嵌套事務,比如前面說到的 methodB/methodC 獨立事務。
什么是嵌套事務?
還是拿上面的例子來說, methodA 中調(diào)用了 methodB,兩個方法都有對數(shù)據(jù)庫的操作,而且都需要事務:
// MethodA:
public void methodA(){
int updated = connection.prepareStatement().executeUpdate();
methodB();
// ...
}
// MethodB:
public void methodB(){
// ...
}
復制代碼**這種多個方法調(diào)用鏈中都有事務的場景,就是嵌套事務。**不過要注意的是,并不是說多個方法使用一個事務才叫嵌套,哪怕是不同的事務,只要在這個方法的調(diào)用鏈中,都是嵌套事務。
什么是事務傳播行為?
那調(diào)用鏈中的子方法,是用一個新事務,還是使用當前事務呢?這個子方法決定使用新事務還是當前事務(或不使用事務)的策略,就叫事務傳播。
在 Spring 的事務管理中,這個子方法的事務處理策略叫做事務傳播行為(Propogation Behavior)。
有哪些事務傳播行為?
Spring 的事務管理支持多種傳播行為,這里就不貼了,八股文里啥都有。
但給這些傳播行為分類之后,無非是以下三種:
優(yōu)先使用當前事務
不使用當前事務,新建事務
不使用任何事務
比如上面的例子中,methodB/methodC 獨立事務,就屬于第 2 種傳播行為 - 不使用當前事務,新建事務
看個栗子
以 Spring JDBC + Spring 注解版的事務舉例。在默認的事務傳播行為下,methodA 和 methodB 會使用同一個 Connection,在一個事務中
@Transactional
public void methodA(){
jdbcTemplate.batchUpdate(updateSql, params);
methodB();
}
@Transactional
public void methodB(){
jdbcTemplate.batchUpdate(updateSql, params);
}
復制代碼如果我想讓 methodB 不使用 methodA 的事務,自己新建一個連接/事務呢?只需要簡單的配置一下 @Transactional 注解:
@Transactional
public void methodA(){
jdbcTemplate.batchUpdate(updateSql, params);
methodB();
}
// 傳播行為配置為 - 方式2,不使用當前事務,獨立一個新事務
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
jdbcTemplate.batchUpdate(updateSql, params);
}
復制代碼就是這么簡單,獲取 Connection/多方法共享 Connection/多方法共享+獨享 Connection/提交/釋放連接之類的操作,完全不需要我們操心,Spring 都替我們做好了。
怎么回滾?
在注解版的事務管理中,默認的的回滾策略是:拋出異常就回滾。這個默認策略挺好,連回滾都幫我們解決了,再也不用手動回滾。
但是如果在嵌套事務中,子方法獨立新事務呢?這個時候哪怕拋出異常,也只能回滾子事務,不能直接影響前一個事務
可如果這個拋出的異常不是 sql 導致的,比如校驗不通過或者其他的異常,此時應該將當前的事務回滾嗎?
這個還真不一定,誰說拋異常就要回滾,異常也不回滾行不行?
當然可以!拋異常和回滾事務本來就是兩個問題,可以連在一起,也可以分開處理
// 傳播行為配置為 - 方式2,不使用當前事務,獨立一個新事務
// 指定 Exception 也不會滾
@Transactional(propagation = Propagation.REQUIRES_NEW, noRollbackFor = Exception.class)
public void methodB(){
jdbcTemplate.batchUpdate(updateSql, params);
}
復制代碼每個事務/連接使用不同配置
除了傳播和回滾之外,還可以給每個事務/連接使用不同的配置,比如不同的隔離級別:
@Transactional
public void methodA(){
jdbcTemplate.batchUpdate(updateSql, params);
methodB();
}
// 傳播行為配置為 - 方式2,不使用當前事務,獨立一個新事務
// 這個事務/連接中使用 RC 隔離級別,而不是默認的 RR
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED)
public void methodB(){
jdbcTemplate.batchUpdate(updateSql, params);
}
復制代碼除了隔離級別之外,其他的 JDBC Connection 配置當然也是支持的,比如 readOnly。這樣一來,雖然我們不用顯示的獲取 connection/session,但還是可以給嵌套中的每一個事務配置不同的參數(shù),非常靈活。
功能總結
好了,現(xiàn)在已經(jīng)了解了 Spring 事務管理的所有核心功能,來總結一下這些核心功能點:
連接/資源管理 - 無需手動獲取資源、共享資源、釋放資源
嵌套事務的支持 - 支持嵌套事務中使用不同的資源策略、回滾策略
每個事務/連接使用不同的配置
事務管理器(TransactionManager)模型
其實仔細想想,事務管理的核心操作只有兩個:提交和回滾。前面所謂的傳播、嵌套、回滾之類的,都是基于這兩個操作。
所以 Spring 將事務管理的核心功能抽象為一個事務管理器(Transaction Manager),基于這個事務管理器核心,可以實現(xiàn)多種事務管理的方式。
這個核心的事務管理器只有三個功能接口:
獲取事務資源,資源可以是任意的,比如jdbc connection/hibernate mybatis session之類,然后綁定并存儲
提交事務 - 提交指定的事務資源
回滾事務 - 回滾指定的事務資源
interface PlatformTransactionManager{
// 獲取事務資源,資源可以是任意的,比如jdbc connection/hibernate mybatis session之類
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
// 提交事務
void commit(TransactionStatus status) throws TransactionException;
// 回滾事務
void rollback(TransactionStatus status) throws TransactionException;
}
復制代碼事務定義 - TransactionDefinition
還記得上面的 @Transactional 注解嗎,里面定義了傳播行為、隔離級別、回滾策略、只讀之類的屬性,這個就是一次事務操作的定義。
在獲取事務資源時,需要根據(jù)這個事務的定義來進行不同的配置:
比如配置了使用新事務,那么在獲取事務資源時就需要創(chuàng)建一個新的,而不是已有的
比如配置了隔離級別,那么在首次創(chuàng)建資源(Connection)時,就需要給 Connection 設置 propagation
比如配置了只讀屬性,那么在首次創(chuàng)建資源(Connection)時,就需要給 Connection 設置 readOnly
為什么要單獨用一個 TransactionDefinition 來存儲事務定義,直接用注解的屬性不行嗎?
當然可以,但注解的事務管理只是 Spring 提供的自動擋,還有適合老司機的手動擋事務管理(后面會介紹);手動擋可用不了注解,所以單獨建一個事務定義的模型,這樣就可以實現(xiàn)通用。
事務狀態(tài) - TransactionStatus
那既然嵌套事務下,每個子方法的事務可能不同,所以還得有一個子方法事務的狀態(tài) - TransactionStatus,用來存儲當前事務的一些數(shù)據(jù)和狀態(tài),比如事務資源(Connection)、回滾狀態(tài)等。
獲取事務資源
事務管理器的第一步,就是根據(jù)事務定義來獲取/創(chuàng)建資源了,這一步最麻煩的是要區(qū)分傳播行為,不同傳播行為下的邏輯不太一樣。
“默認的傳播行為下,使用當前事務”,怎么算有當前事務呢?
把事務資源存起來嘛,只要已經(jīng)存在那就是有當前事務,直接獲取已存儲的事務資源就行。文中開頭的例子也演示了,如果想讓多個方法無感的使用同一個事務,可以用 ThreadLocal 存儲起來,簡單粗暴。
Spring 也是這么做的,不過它實現(xiàn)的更復雜一些,抽象了一層事務資源同步管理器 - TransactionSynchronizationManager(本文后面會簡稱 TxSyncMgr),在這個同步管理器里使用 ThreadLocal 存儲了事務資源(本文為了方便理解,盡可能的不貼非關鍵源碼)。
剩下的就是根據(jù)不同傳播行為,執(zhí)行不同的策略了,分類之后只有 3 個條件分支:
當前有事務 - 根據(jù)不同傳播行為處理不同
當前沒事務,但需要開啟新事務
徹底不用事務 - 這個很少用
public final TransactionStatus getTransaction(TransactionDefinition definition) {
//創(chuàng)建事務資源 - 比如 Connection
Object transaction = doGetTransaction();
if (isExistingTransaction(transaction)) {
// 處理當前已有事務的場景
return handleExistingTransaction(def, transaction, debugEnabled);
}else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED){
// 開啟新事務
return startTransaction(def, transaction, debugEnabled, suspendedResources);
}else {
// 徹底不用事務
}
// ...
}
復制代碼先介紹一下分支 2 - 當前沒事務,但需要開啟新事務,這個邏輯相對簡單一些。只需要新建事務資源,然后綁定到 ThreadLocal 即可:
private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
boolean debugEnabled, SuspendedResourcesHolder suspendedResources) {
// 創(chuàng)建事務
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
// 開啟事務(beginTx或者setAutoCommit之類的操作)
// 然后將事務資源綁定到事務資源管理器 TransactionSynchronizationManager
doBegin(transaction, definition);
復制代碼現(xiàn)在回到分支 1 - 當前有事務 - 根據(jù)不同傳播行為處理不同,這個就稍微有點麻煩了。因為有子方法獨立事務的需求,可是 TransactionSynchronizationManager 卻只能存一個事務資源。
掛起(Suspend)和恢復(Resume)
Spring 采用了一種**掛起(Suspend) - 恢復(Resume)**的設計來解決這個嵌套資源處理的問題。當子方法需要獨立事務時,就將當前事務掛起,從 TxSyncMgr 中移除當前事務資源,創(chuàng)建新事務的狀態(tài)時,將掛起的事務資源保存至新的事務狀態(tài) TransactionStatus 中;在子方法結束時,只需要再從子方法的事務狀態(tài)中,再次拿出掛起的事務資源,重新綁定至 TxSyncMgr 即可完成恢復的操作。
整個掛起 - 恢復的流程,如下圖所示:
注意:掛起操作是在獲取事務資源這一步做的,而恢復的操作是在子方法結束時(提交或者回滾)中進行的。
這樣一來,每個 TransactionStatus 都會保存掛起的前置事務資源,如果方法調(diào)用鏈很長,每次都是新事務的話,那這個 TransactionStatus 看起來就會像一個鏈表:
提交事務
獲取資源、操作完畢后來到了提交事務這一步,這個提交操作比較簡單,只有兩步:
當前是新事務才提交
處理掛起資源
怎么知道是新事務?
每經(jīng)過一次事務嵌套,都會創(chuàng)建一個新的 TransactionStatus,這個事務狀態(tài)里會記錄當前是否是新事務。如果多個子方法都使用一個事務資源,那么除了第一個創(chuàng)建事務資源的 TransactionStatus 之外,其他都不是新事務。
如下圖所示,A -> B -> C 時,由于 BC 都使用當前事務,那么雖然 ABC 所使用的事務資源是一樣的,但是只有 A 的 TransactionStatus 是新事務,BC 并不是;那么在 BC 提交事務時,就不會真正的調(diào)用提交,只有回到 A 執(zhí)行 commit 操作時,才會真正的調(diào)用提交操作。
這里再解釋下,為什么新事務才需要提交,而已經(jīng)有事務卻什么都不用做:
因為對于新事務來說,這里的提交操作已經(jīng)是事務完成了;而對于非新事務的場景,前置事務(即當前事務)還沒有執(zhí)行完,可能后面還有其他數(shù)據(jù)庫操作,所以這個提交的操作得讓當前事務創(chuàng)建方去做,這里并不能提交。
回滾事務
除了提交,還有回滾呢,回滾事務的邏輯和提交事務類似:
如果是新事務才回滾,原因上面已經(jīng)介紹過了
如果不是新事務則只設置回滾標記
處理掛起資源
注意:事務管理器是不包含回滾策略這個東西的,回滾策略是 AOP 版的事務管理增強的功能,但這個功能并不屬于核心的事務管理器
自動擋與手動擋
Spring 的事務管理功能都是圍繞著上面這個事務管理器運行的,提供了三種管理事務的方式,分別是:
XML AOP 的事務管理 - 比較古老現(xiàn)在用的不多
注解版本的事務管理 - @Transactional
TransactionTemplate - 手動擋的事務管理,也稱編程式事務管理
自動擋
XML/@Transactional 兩種基于 AOP 的注解管理,其入口類是 TransactionInterceptor,是一個 AOP 的 Interceptor,負責調(diào)用事務管理器來實現(xiàn)事務管理。
因為核心功能都在事務管理器里實現(xiàn),所以這個 AOP Interceptor 很簡單,只是調(diào)用一下事務管理器,核心(偽)代碼如下:
public Object invoke(MethodInvocation invocation) throws Throwable {
// 獲取事務資源
Object transaction = transactionManager.getTransaction(txAttr);
Object retVal;
try {
// 執(zhí)行業(yè)務代碼
retVal = invocation.proceedWithInvocation();
// 提交事務
transactionManager.commit(txStatus);
} catch (Throwable ex){
// 先判斷異常回滾策略,然后調(diào)用事務管理器的 rollback
rollbackOn(ex, txStatus);
}
}
復制代碼并且 AOP 這種自動擋的事務管理還增加了一個回滾策略的玩法,這個是手動擋 TransactionTemplate 所沒有的,但這個功能并不在事務管理器中,只是 AOP 版事務的一個增強。
手動擋
TransactionTemplate 這個是手動擋的事務管理,雖然沒有注解的方便,但是好在靈活,異常/回滾啥的都可以自己控制。
所以這個實現(xiàn)更簡單,連異常回滾策略都沒有,特殊的回滾方式還要自己設置(默認是任何異常都會回滾),核心(偽)代碼如下:
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
// 獲取事務資源
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
// 執(zhí)行 callback 業(yè)務代碼
result = action.doInTransaction(status);
}
catch (Throwable ex) {
// 調(diào)用事務管理器的 rollback
rollbackOnException(status, ex);
}
提交事務
this.transactionManager.commit(status);
}
}
復制代碼為什么有這么方便的自動擋,還要手動擋?
因為自動擋更靈活啊,想怎么玩就怎么玩,比如我可以在一個方法中,執(zhí)行多個數(shù)據(jù)庫操作,但使用不同的事務資源:
Integer rows = new TransactionTemplate((PlatformTransactionManager) transactionManager,
new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus status) {
// update 0
int rows0 = jdbcTemplate.update(...);
// update 1
int rows1 = jdbcTemplate.update(...);
return rows0 + rows1;
}
});
Integer rows2 = new TransactionTemplate((PlatformTransactionManager) transactionManager,
new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus status) {
// update 2
int rows2 = jdbcTemplate.update(...);
return rows2;
}
});
復制代碼在上面這個例子里,通過 TransactionTemplate 我們可以精確的控制 update0/update1 使用同一個事務資源和隔離級別,而 update2 單獨使用一個事務資源,并且不需要新建類加注解的方式。
手自一體可以嗎?
當然可以,只要我們使用的是同一個事務管理器的實例,因為綁定資源到同步資源管理器這個操作是在事務管理器中進行的。
AOP 版本的事務管理里,同樣可以使用手動擋的事務管理繼續(xù)操作,而且還可以使用同一個事務資源 。
比如下面這段代碼,update1/update2 仍然在一個事務內(nèi),并且 update2 的 callback 結束后并不會提交事務,事務最終會在 methodA 結束時,TransactionInterceptor 中才會提交
@Transactional
public void methodA(){
// update 1
jdbcTemplate.update(...);
new TransactionTemplate((PlatformTransactionManager) transactionManager,
new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus status) {
// update 2
int rows2 = jdbcTemplate.update(...);
return rows2;
}
});
}
復制代碼總結
Spring 的事務管理,其核心是一個抽象的事務管理器,XML/@Transactional/TransactionTemplate 幾種方式都是基于這個事務管理器的,三中方式的核心實現(xiàn)區(qū)別并不大,只是入口不同而已。
本文為了方便理解,省略了大量的非關鍵實現(xiàn)細節(jié),可能會導致有部分描述不嚴謹?shù)牡胤剑缬袉栴}歡迎評論區(qū)留言。
作者:空無
鏈接:https://juejin.cn/post/6970814895021359141
來源:掘金
著作權歸作者所有。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。
