事務管理,一個被說爛的也被看爛的話題,還是八股文中的基礎股之一。
本文會從設計角度,一步步的剖析 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();
}
現在看起來好點了,不過我有一個新的需求:想讓 methodB 獨立一個新事務,單獨提交和回滾,不影響 methodA
這……可就有點難搞了,ThreadLocal 中已經繫結了一個 Connection,再新事務的話就不好辦了
那如果再複雜點呢,methodB 中需要呼叫 methodC,methodC 也需要一個獨立事務……
而且,每次 bind/unbind 的操作也有點太傻了,萬一哪個方法忘了寫 unbind ,最後來一個連線洩露那不是完蛋了!
好在 Spring 提供了事務管理器,幫我們解決了這一系列痛點。
Spring 提供的事務管理可以幫我們管理事務相關的資源,比如 JDBC 的 Connection、Hibernate 的 Session、Mybatis 的 SqlSession。如說上面的 Connection 繫結到 ThreadLocal 來解決共用一個事務的這種方式,Spring 事務管理就已經幫我們做好了。
還可以幫我們處理複雜場景下的巢狀事務,比如前面說到的 methodB/methodC 獨立事務。
還是拿上面的例子來說, methodA 中呼叫了 methodB,兩個方法都有對資料庫的操作,而且都需要事務:
// MethodA:
public void methodA(){
int updated = connection.prepareStatement().executeUpdate();
methodB();
// ...
}
// MethodB:
public void methodB(){
// ...
}
這種多個方法呼叫鏈中都有事務的場景,就是巢狀事務。不過要注意的是,並不是說多個方法使用一個事務才叫巢狀,哪怕是不同的事務,只要在這個方法的呼叫鏈中,都是巢狀事務。
那呼叫鏈中的子方法,是用一個新事務,還是使用當前事務呢?這個子方法決定使用新事務還是當前事務(或不使用事務)的策略,就叫事務傳播。
在 Spring 的事務管理中,這個子方法的事務處理策略叫做事務傳播行為(Propogation Behavior)。
Spring 的事務管理支援多種傳播行為,這裡就不貼了,八股文裡啥都有。
但給這些傳播行為分類之後,無非是以下三種:
優先使用當前事務
不使用當前事務,新建事務
不使用任何事務
比如上面的例子中,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,但還是可以給巢狀中的每一個事務設定不同的引數,非常靈活。
好了,現在已經瞭解了 Spring 事務管理的所有核心功能,來總結一下這些核心功能點:
連線/資源管理 - 無需手動獲取資源、共用資源、釋放資源
巢狀事務的支援 - 支援巢狀事務中使用不同的資源策略、回滾策略
每個事務/連線使用不同的設定
其實仔細想想,事務管理的核心操作只有兩個:提交和回滾。前面所謂的傳播、巢狀、回滾之類的,都是基於這兩個操作。
所以 Spring 將事務管理的核心功能抽象為一個事務管理器(Transaction Manager),基於這個事務管理器核心,可以實現多種事務管理的方式。
這個核心的事務管理器只有三個功能介面:
獲取事務資源,資源可以是任意的,比如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;
}
還記得上面的 @Transactional 註解嗎,裡面定義了傳播行為、隔離級別、回滾策略、唯讀之類的屬性,這個就是一次事務操作的定義。
在獲取事務資源時,需要根據這個事務的定義來進行不同的設定:
比如設定了使用新事務,那麼在獲取事務資源時就需要建立一個新的,而不是已有的
比如設定了隔離級別,那麼在首次建立資源(Connection)時,就需要給 Connection 設定 propagation
比如設定了唯讀屬性,那麼在首次建立資源(Connection)時,就需要給 Connection 設定 readOnly
為什麼要單獨用一個 TransactionDefinition 來儲存事務定義,直接用註解的屬性不行嗎?
當然可以,但註解的事務管理只是 Spring 提供的自動擋,還有適合老司機的手動擋事務管理(後面會介紹);手動擋可用不了註解,所以單獨建一個事務定義的模型,這樣就可以實現通用。
那既然巢狀事務下,每個子方法的事務可能不同,所以還得有一個子方法事務的狀態 - TransactionStatus,用來儲存當前事務的一些資料和狀態,比如事務資源(Connection)、回滾狀態等。
事務管理器的第一步,就是根據事務定義來獲取/建立資源了,這一步最麻煩的是要區分傳播行為,不同傳播行為下的邏輯不太一樣。
「預設的傳播行為下,使用當前事務」,怎麼算有當前事務呢?
把事務資源存起來嘛,只要已經存在那就是有當前事務,直接獲取已儲存的事務資源就行。文中開頭的例子也演示了,如果想讓多個方法無感的使用同一個事務,可以用 ThreadLocal 儲存起來,簡單粗暴。
Spring 也是這麼做的,不過它實現的更復雜一些,抽象了一層事務資源同步管理器 - TransactionSynchronizationManager(本文後面會簡稱 TxSyncMgr),在這個同步管理器裡使用 ThreadLocal 儲存了事務資源(本文為了方便理解,儘可能的不貼非關鍵原始碼)。
剩下的就是根據不同傳播行為,執行不同的策略了,分類之後只有 3 個條件分支:
當前有事務 - 根據不同傳播行為處理不同
當前沒事務,但需要開啟新事務
徹底不用事務 - 這個很少用
public final TransactionStatus getTransaction(TransactionDefinition definition) {
//建立事務資源 - 比如 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) {
// 建立事務
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
// 開啟事務(beginTx或者setAutoCommit之類的操作)
// 然後將事務資源繫結到事務資源管理器 TransactionSynchronizationManager
doBegin(transaction, definition);
現在回到分支1 - 當前有事務 - 根據不同傳播行為處理不同,這個就稍微有點麻煩了。因為有子方法獨立事務的需求,可是 TransactionSynchronizationManager 卻只能存一個事務資源。
Spring 採用了一種掛起(Suspend) - 恢復(Resume)的設計來解決這個巢狀資源處理的問題。當子方法需要獨立事務時,就將當前事務掛起,從 TxSyncMgr 中移除當前事務資源,建立新事務的狀態時,將掛起的事務資源儲存至新的事務狀態 TransactionStatus 中;在子方法結束時,只需要再從子方法的事務狀態中,再次拿出掛起的事務資源,重新系結至 TxSyncMgr 即可完成恢復的操作。
整個掛起 - 恢復的流程,如下圖所示:
注意:掛起操作是在獲取事務資源這一步做的,而恢復的操作是在子方法結束時(提交或者回滾)中進行的。
這樣一來,每個 TransactionStatus 都會儲存掛起的前置事務資源,如果方法呼叫鏈很長,每次都是新事務的話,那這個 TransactionStatus 看起來就會像一個連結串列:
獲取資源、操作完畢後來到了提交事務這一步,這個提交操作比較簡單,只有兩步:
當前是新事務才提交
處理掛起資源
怎麼知道是新事務?
每經過一次事務巢狀,都會建立一個新的 TransactionStatus,這個事務狀態裡會記錄當前是否是新事務。如果多個子方法都使用一個事務資源,那麼除了第一個建立事務資源的 TransactionStatus 之外,其他都不是新事務。
如下圖所示,A -> B -> C 時,由於 BC 都使用當前事務,那麼雖然 ABC 所使用的事務資源是一樣的,但是隻有 A 的 TransactionStatus 是新事務,BC 並不是;那麼在 BC 提交事務時,就不會真正的呼叫提交,只有回到 A 執行 commit 操作時,才會真正的呼叫提交操作。
這裡再解釋下,為什麼新事務才需要提交,而已經有事務卻什麼都不用做:
因為對於新事務來說,這裡的提交操作已經是事務完成了;而對於非新事務的場景,前置事務(即當前事務)還沒有執行完,可能後面還有其他資料庫操作,所以這個提交的操作得讓當前事務建立方去做,這裡並不能提交。
除了提交,還有回滾呢,回滾事務的邏輯和提交事務類似:
如果是新事務才回滾,原因上面已經介紹過了
如果不是新事務則只設定回滾標記
處理掛起資源
注意:事務管理器是不包含回滾策略這個東西的,回滾策略是 AOP 版的事務管理增強的功能,但這個功能並不屬於核心的事務管理器
Spring 的事務管理功能都是圍繞著上面這個事務管理器執行的,提供了三種管理事務的方式,分別是:
XML AOP 的事務管理 - 比較古老現在用的不多
註解版本的事務管理 - @Transactional
TransactionTemplate - 手動擋的事務管理,也稱程式設計式事務管理
XML/@Transactional 兩種基於 AOP 的註解管理,其入口類是 TransactionInterceptor,是一個 AOP 的 Interceptor,負責呼叫事務管理器來實現事務管理。
因為核心功能都在事務管理器裡實現,所以這個 AOP Interceptor 很簡單,只是呼叫一下事務管理器,核心(偽)程式碼如下:
public Object invoke(MethodInvocation invocation) throws Throwable {
// 獲取事務資源
Object transaction = transactionManager.getTransaction(txAttr);
Object retVal;
try {
// 執行業務程式碼
retVal = invocation.proceedWithInvocation();
// 提交事務
transactionManager.commit(txStatus);
} catch (Throwable ex){
// 先判斷異常回滾策略,然後呼叫事務管理器的 rollback
rollbackOn(ex, txStatus);
}
}
並且 AOP 這種自動擋的事務管理還增加了一個回滾策略的玩法,這個是手動擋 TransactionTemplate 所沒有的,但這個功能並不在事務管理器中,只是 AOP 版事務的一個增強。
TransactionTemplate
這個是手動擋的事務管理,雖然沒有註解的方便,但是好在靈活,異常/回滾啥的都可以自己控制。
所以這個實現更簡單,連異常回滾策略都沒有,特殊的回滾方式還要自己設定(預設是任何異常都會回滾),核心(偽)程式碼如下:
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
// 獲取事務資源
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
// 執行 callback 業務程式碼
result = action.doInTransaction(status);
}
catch (Throwable ex) {
// 呼叫事務管理器的 rollback
rollbackOnException(status, ex);
}
提交事務
this.transactionManager.commit(status);
}
}
因為手動擋更靈活啊,想怎麼玩就怎麼玩,比如我可以在一個方法中,執行多個資料庫操作,但使用不同的事務資源:
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 版本的事務管理裡,同樣可以使用手動擋的事務管理繼續操作,而且還可以使用同一個事務資源 。
比如下面這段程式碼,update1/update2 仍然在一個事務內,並且 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 幾種方式都是基於這個事務管理器的,三中方式的核心實現區別並不大,只是入口不同而已。
本文為了方便理解,省略了大量的非關鍵實現細節,可能會導致有部分描述不嚴謹的地方,如有問題歡迎評論區留言。
作者:京東保險 蔣信
來源:京東雲開發者社群 轉載請註明來源