作者:avengerEug
連結:https://juejin.cn/post/6984574787511123999
前言
經過前面對Spring AOP、事務的總結,我們已經對它們有了一個比較感性的認知了。
今天,我繼續安利一個獨門絕技:Spring 事務的鉤子函式。單純的講技術可能比較枯燥乏味。接下來,我將以一個實際的案例來描述Spring事務鉤子函式的正確使用姿勢。
一、案例背景
拿支付系統相關的業務來舉例。
在支付系統中,我們需要記錄每個賬戶的資金流水(記錄使用者A因為哪個操作扣了錢,因為哪個操作加了錢),這樣我們才能對每個賬戶的賬做到心中有數,對於支付系統而言,資金流水的資料可謂是最重要的。
因此,為了防止支付系統的老大徇私舞弊,CTO提了一個流水存檔的需求:要求支付系統對每個賬戶的資金流水做一份存檔,要求支付系統在寫流水的時候,把流水相關的資訊以訊息的形式推送到kafka,由存檔系統消費這個訊息並落地到庫裡(這個庫只有存檔系統擁有寫許可權)。
推薦一個開源免費的 Spring Boot 實戰專案:
https://github.com/javastacks/spring-boot-best-practice
整個需求的流程如下所示:
整個需求的流程還是比較簡單的,考慮到後續會有其他事業部也要進行資料存檔操作,CTO建議支付系統團隊內部開發一個二方庫,這個二方庫的主要功能就是傳送訊息到kafka中去。
二、確定方案
既然要求開發一個二方庫,因此,我們需要考慮如下幾件事情:
1、技術棧使用的springboot,因此,這裡最好以starter的方式提供
2、二方庫需要傳送訊息給kafka,最好是二方庫內部基於kafka生產者的api建立生產者,不要使用Spring自帶的kafkaTemplate,因為整合方有可能已經使用了kafkaTemplate。不能與整合方造成衝突。
3、減少對接方的整合難度、學習成本,最好是提供一個簡單實用的api,業務側能簡單上手。
4、傳送訊息這個操作需要支援事務,儘量不影響主業務
在上述的幾件事情中,最需要注意的應該就是第4點:傳送訊息這個操作需要支援事務,儘量不影響主業務。
這是什麼意思呢?
首先,儘量不影響主業務,這個最簡單的方式就是使用非同步機制。
其次,需要支援事務是指:假設我們的api是在事務方法內部呼叫的,那麼我們需要保證事務提交後再執行這個api。那麼,我們的流水落地api應該要有這樣的功能:
內部可以判斷當前是否存在事務,如果存在事務,則需要等事務提交後再非同步傳送訊息給kafka。
如果不存在事務則直接非同步傳送訊息給kafka。而且這樣的判斷邏輯得放在二方庫內部才行。那現在擺在我們面前的問題就是:我要如何判斷當前是否存在事務,以及如何在事務提交後再觸發我們自定義的邏輯呢?
三、TransactionSynchronizationManager顯神威
這個類內部所有的變數、方法都是static修飾的,也就是說它其實是一個工具類。是一個事務同步器。下述是流水落地API的虛擬碼,這段程式碼就解決了我們上述提到的疑問:
private final ExecutorService executor = Executors.newSingleThreadExecutor();
public void sendLog() {
// 判斷當前是否存在事務
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
// 無事務,非同步傳送訊息給kafka
executor.submit(() -> {
// 傳送訊息給kafka
try {
// 傳送訊息給kafka
} catch (Exception e) {
// 記錄異常資訊,發郵件或者進入待處理列表,讓開發人員感知異常
}
});
return;
}
// 有事務,則新增一個事務同步器,並重寫afterCompletion方法(此方法在事務提交後會做回撥)
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_COMMITTED) {
// 事務提交後,再非同步傳送訊息給kafka
executor.submit(() -> {
try {
// 傳送訊息給kafka
} catch (Exception e) {
// 記錄異常資訊,發郵件或者進入待處理列表,讓開發人員感知異常
}
});
}
}
});
}
程式碼比較簡單,其主要是TransactionSynchronizationManager的使用。
推薦一個開源免費的 Spring Boot 實戰專案:
https://github.com/javastacks/spring-boot-best-practice
3.1、判斷是否存在事務?
TransactionSynchronizationManager.isSynchronizationActive() 方法顯神威
我們先看下這個方法的原始碼:
// TransactionSynchronizationManager.java類內部的部分程式碼
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
public static boolean isSynchronizationActive() {
return (synchronizations.get() != null);
}
很明顯,synchronizations是一個執行緒變數(ThreadLocal)。那它是在什麼時候set進去的呢?
這裡的話,可以參考下這個方法:org.springframework.transaction.support.TransactionSynchronizationManager#initSynchronization,其原始碼如下所示:
/**
* Activate transaction synchronization for the current thread.
* Called by a transaction manager on transaction begin.
* @throws IllegalStateException if synchronization is already active
*/
public static void initSynchronization() throws IllegalStateException {
if (isSynchronizationActive()) {
throw new IllegalStateException("Cannot activate transaction synchronization - already active");
}
logger.trace("Initializing transaction synchronization");
synchronizations.set(new LinkedHashSet<>());
}
由原始碼中的註釋也可以知道,它是在事務管理器開啟事務時呼叫的。
換句話說,只要我們的程式執行到帶有事務特性的方法時,就會線上程變數中放入一個LinkedHashSet,用來標識當前存在事務。只要isSynchronizationActive返回true,則代表當前有事務。
因此,結合這兩個方法我們是指能解決我們最開始提出的疑問:要如何判斷當前是否存在事務
3.2、如何在事務提交後觸發自定義邏輯?
TransactionSynchronizationManager.registerSynchronization()方法顯神威
我們來看下這個方法的原始碼:
/**
* Register a new transaction synchronization for the current thread.
* Typically called by resource management code.
* <p>Note that synchronizations can implement the
* {@link org.springframework.core.Ordered} interface.
* They will be executed in an order according to their order value (if any).
* @param synchronization the synchronization object to register
* @throws IllegalStateException if transaction synchronization is not active
* @see org.springframework.core.Ordered
*/
public static void registerSynchronization(TransactionSynchronization synchronization)
throws IllegalStateException {
Assert.notNull(synchronization, "TransactionSynchronization must not be null");
if (!isSynchronizationActive()) {
throw new IllegalStateException("Transaction synchronization is not active");
}
synchronizations.get().add(synchronization);
}
這裡又使用到了synchronizations執行緒變數,我們在判斷是否存在事務時,就是判斷這個執行緒變數內部是否有值。那我們現在想在事務提交後觸發自定義邏輯和這個有什麼關係呢?
我們在上面構建流水落地api的虛擬碼中有向synchronizations內部新增了一個TransactionSynchronizationAdapter,內部並重寫了afterCompletion方法,其程式碼如下所示:
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_COMMITTED) {
// 事務提交後,再非同步傳送訊息給kafka
executor.submit(() -> {
try {
// 傳送訊息給kafka
} catch (Exception e) {
// 記錄異常資訊,發郵件或者進入待處理列表,讓開發人員感知異常
}
});
}
}
});
我們結合registerSynchronization的原始碼來看,其實這段程式碼主要就是向執行緒變數內部的LinkedHashSet新增了一個物件而已,但就是這麼一個操作,讓Spring在事務執行的過程中變得“有事情可做”
。這是什麼意思呢?
是因為Spring在執行事務方法時,對於操作事務的每一個階段都有一個回撥操作,比如:trigger系列的回撥
invoke系列的回撥
而我們現在的需求就是在事務提交後觸發自定義的函式,那就是在invokeAfterCommit和invokeAfterCompletion這兩個方法來選了。首先,這兩個方法都會拿到所有TransactionSynchronization的集合(其中會包括我們上述新增的TransactionSynchronizationAdapter)。
但是要注意一點:invokeAfterCommit只能拿到集合,invokeAfterCompletion除了集合還有一個int型別的引數,而這個int型別的引數其實是當前事務的一種狀態。也就是說,如果我們重寫了invokeAfterCompletion方法,我們除了能拿到集合外,還能拿到當前事務的狀態。
因此,此時我們可以根據這個狀態來做不同的事情,比如:可以在事務提交時做自定義處理,也可以在事務回滾時做自定義處理等等。
四、總結
上面有說到,我們判斷當前是否存在事務、新增鉤子函式都是依賴執行緒變數的。因此,我們在使用過程中,一定要避免切換執行緒。否則會出現不生效的情況。
更多文章推薦:
1.Spring Boot 3.x 教程,太全了!
2.2,000+ 道 Java面試題及答案整理(2024最新版)
3.免費獲取 IDEA 啟用碼的 7 種方式(2024最新版)
覺得不錯,別忘了隨手點贊+轉發哦!