Spring TransactionalEventListener 資料無法儲存問題完整解析
前言
在 Spring 應用程式中,@TransactionalEventListener
是一個非常實用的功能,可以讓我們在交易的特定階段執行特定邏輯。然而今天遇到一個常見問題:程式碼明明有執行,debug 也確實走到了資料庫儲存的程式碼,但最終資料卻沒有儲存到資料庫中。
Spring Transaction 基礎概念
什麼是 Spring Transaction?
Spring Transaction 管理是 Spring Framework 的核心功能之一,它提供了宣告式的交易管理機制。透過 @Transactional
註解,我們可以輕鬆地為方法添加交易支援。
Transaction Propagation 傳播行為
Spring 提供了多種交易傳播行為:
傳播行為 | 描述 |
---|---|
REQUIRED |
如果當前有交易,就加入該交易;如果沒有,就建立新交易(預設) |
REQUIRES_NEW |
總是建立新交易,如果當前有交易就暫停它 |
SUPPORTS |
如果當前有交易就加入,沒有就以非交易方式執行 |
NOT_SUPPORTED |
以非交易方式執行,如果當前有交易就暫停它 |
MANDATORY |
必須在交易中執行,如果沒有交易就拋出異常 |
NEVER |
不能在交易中執行,如果有交易就拋出異常 |
NESTED |
在當前交易中建立巢狀交易 |
TransactionalEventListener 介紹
基本概念
@TransactionalEventListener
是 Spring 4.2 引入的功能,允許我們在交易的特定階段監聽和處理事件。
執行階段 (TransactionPhase)
1 |
|
問題場景重現
程式碼結構
我們的系統架構如下:
1 |
|
1 |
|
問題分析與流程圖
執行流程圖
sequenceDiagram
participant Client
participant ReservationService
participant Database
participant EventPublisher
participant EventListener
participant AuditService
participant AuditHandler
Client->>ReservationService: saveReservation()
Note over ReservationService: @Transactional 開始
ReservationService->>Database: 儲存預約資料
Database-->>ReservationService: 儲存成功
ReservationService->>EventPublisher: publishEvent()
Note over EventPublisher: 事件已發布,但尚未處理
ReservationService-->>Client: 回傳結果
Note over ReservationService: @Transactional 提交
Note over EventListener: AFTER_COMMIT 階段開始
EventPublisher->>EventListener: handleReservationSmsEvent()
EventListener->>BecomeService: sendMessage()
BecomeService-->>EventListener: 簡訊發送完成
EventListener->>AuditService: createAudit()
AuditService->>AuditHandler: handle()
Note over AuditHandler: 沒有 @Transactional!
AuditHandler->>Database: save() - 失敗!
Note over Database: 資料沒有儲存
問題核心
關鍵問題在於:
- 原始交易已結束:當
AFTER_COMMIT
階段執行時,原本的@Transactional
已經提交並結束 - 新操作沒有交易:Listener 中的資料庫操作沒有在任何交易中執行
- 資料庫連接問題:沒有交易的資料庫操作可能不會真正提交到資料庫
詳細解決方案
解決方案 1:在 Handler 層級加上交易(推薦)
1 |
|
解決方案 2:在 Service 層級加上交易
1 |
|
解決方案 3:調整事件監聽階段
如果希望在原始交易中執行 audit log:
1 |
|
為什麼必須使用 REQUIRES_NEW?
SUPPORTS 的問題
1 |
|
執行流程:
- Listener 在
AFTER_COMMIT
階段執行 - 此時原始交易已經結束,沒有活躍的交易
SUPPORTS
會以非交易方式執行- 資料庫操作可能不會提交 ❌
REQUIRES_NEW 的優勢
1 |
|
執行流程:
- 不管當前是否有交易,都建立新的交易
- 在新交易中執行資料庫操作
- 新交易獨立提交
- 資料確實儲存到資料庫 ✅
最佳實踐建議
1. 明確交易邊界
1 |
|
2. 異常處理策略
1 |
|
3. 效能考量
1 |
|
4. 監控和日誌
1 |
|
常見陷阱與注意事項
1. 交易傳播設定錯誤
❌ 錯誤做法:
1 |
|
✅ 正確做法:
1 |
|
2. 忘記處理異常
❌ 錯誤做法:
1 |
|
✅ 正確做法:
1 |
|
3. 阻塞主要業務流程
❌ 錯誤做法:
1 |
|
✅ 正確做法:
1 |
|
總結
使用 @TransactionalEventListener
時,最重要的是理解交易的生命週期和事件執行的時機:
AFTER_COMMIT
階段沒有交易:原始交易已經結束,需要新建交易- 使用
REQUIRES_NEW
:確保在新交易中執行資料庫操作 - 適當的異常處理:避免影響主要業務流程
- 考慮效能影響:使用非同步處理避免阻塞
透過正確的交易管理和事件處理,我們可以建立穩健且高效的事件驅動系統。
Spring TransactionalEventListener 資料無法儲存問題完整解析
https://shengshengyang.github.io/2025/07/24/transactional-event-listener/