在一個訂單處理介面中,流程如下:
消費端收到訊息後查詢資料庫,發現訂單狀態仍為 1,導致業務邏輯判斷錯誤。
@Service("orderApi")
public class OrderApiImpl implements OrderApi {
@Resource MqService mqService;
@Resource OrderDao orderDao;
public void push(String orderId) {
updateStatus(orderId, 3); // 1. 更新狀態
mqService.produce(orderId); // 2. 發送訊息
}
public void updateStatus(String orderId, Integer status) {
orderDao.updateStatus(orderId, status);
}
}
類別上隱含 @Transactional(propagation = PROPAGATION_REQUIRED),導致整個 push() 在同一個事務中執行。
commit。produce())發生在事務 提交之前。若在 push() 末尾加上 Thread.sleep(10000),現象更明顯:消費端一定讀到舊狀態。
PROPAGATION_REQUIRES_NEW@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateStatus(String orderId, Integer status) { ... }
為何失效? Spring 透過 動態代理 實現事務,同一類別內部方法互呼叫不會經過代理,故新事務不會開啟。
@Service("orderExtApi")
public class OrderExtApiImpl {
@Resource OrderApi orderApi;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateStatusNewPropagation(String orderId, Integer status) {
orderApi.updateStatus(orderId, status);
}
}
再修改原 OrderApiImpl:
@Service("orderApi")
public class OrderApiImpl implements OrderApi {
@Resource MqService mqService;
@Resource OrderDao orderDao;
@Resource OrderExtApiImpl orderExtApi; // 注入新 Bean
public void push(String orderId) {
// 1. 先在獨立事務中更新狀態並提交
orderExtApi.updateStatusNewPropagation(orderId, 3);
// 2. 發送訊息,並檢查回傳結果
Response response = mqService.produce(orderId);
if (response.getCode() != 1) {
log.warn("MQ 發送失敗:" + response.getErrorMsg());
// 3. 發送失敗則回滾狀態(再開一個新事務)
orderExtApi.updateStatusNewPropagation(orderId, 1);
}
}
// ...
}
| 做法 | 是否有效 | 說明 |
|---|---|---|
同類別內部呼叫 @Transactional 方法 | ❌ | 繞過代理,事務不生效 |
| 抽離到另一個 Spring Bean 呼叫 | ✅ | 經過代理,新事務正常開啟 |
發送 MQ 前先 commit 資料庫 | ✅ | 確保消費端讀到最新資料 |
| MQ 發送失敗補償(回滾狀態) | ✅ | 保證最終一致性 |
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 在事務提交後再發送訊息(Spring 4.2+)。Spring 事務與 MQ 整合的核心原則是 「資料庫先提交,訊息後發送」。透過獨立 Bean 開啟 REQUIRES_NEW 事務,或採用事件監聽器 / Outbox 模式,皆能有效避免「訊息跑在資料前」的經典坑。