首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Spring 事務與訊息佇列一致性陷阱:問題分析與解決方案

     編輯:Java 相關知識

情境重現

在一個訂單處理介面中,流程如下:

  1. 更新訂單狀態(如 1 → 3)
  2. 發送訊息至 MQ(通知下游系統)

消費端收到訊息後查詢資料庫,發現訂單狀態仍為 1,導致業務邏輯判斷錯誤。

程式碼示例(有 Bug 版本)

@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() 在同一個事務中執行。

根因分析

  • Spring 事務預設在方法 正常返回後 才 commit。
  • MQ 發送訊息(produce())發生在事務 提交之前。
  • 消費端可能比生產端事務提交更早輪詢到訊息,讀取到的仍是舊資料(隔離級別下的未提交讀或提交讀)。

若在 push() 末尾加上 Thread.sleep(10000),現象更明顯:消費端一定讀到舊狀態。

解決方案:確保資料庫操作先於訊息發送提交

錯誤嘗試:直接在 updateStatus 加 PROPAGATION_REQUIRES_NEW

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateStatus(String orderId, Integer status) { ... }

為何失效? Spring 透過 動態代理 實現事務,同一類別內部方法互呼叫不會經過代理,故新事務不會開啟。

正確做法:抽離到獨立 Bean

@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 發送失敗補償(回滾狀態)✅保證最終一致性

進階替代方案(不修改程式碼結構)

  • Transactional Event Listener:@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 在事務提交後再發送訊息(Spring 4.2+)。
  • Outbox Pattern:在同一事務寫入「訊息表」,再由獨立排程器發送,徹底解決分散式一致性。
  • 本地訊息表 + 定時掃描:同上,適合不引入外部元件的輕量級場景。

結語

Spring 事務與 MQ 整合的核心原則是 「資料庫先提交,訊息後發送」。透過獨立 Bean 開啟 REQUIRES_NEW 事務,或採用事件監聽器 / Outbox 模式,皆能有效避免「訊息跑在資料前」的經典坑。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved