JBoss EAP 10:MDB 到資料庫,讀懂交易失敗、重送與重複處理
訊息重送兩次,資料庫就一定新增兩筆嗎?不一定。但如果只看 onMessage 沒有拋錯,也無法知道資料是否提交、MQ 是否完成確認。這一課沿 MDB → Service → DB 拆開責任,再用本機模型觀察兩個失敗位置:DB 提交前,以及 DB 已提交但確認紀錄尚未完成。
下載第 10 課:H2 交易與合成重送模型下載包會真的執行 Spring JDBC/H2 交易;傳遞次數、ack 與隔離失敗訊息則是測試裡的合成模型,沒有 MDB、RAR、JMS client 或 MQ broker。它能幫你重現資料庫側的結果,不能拿來證明 EAP 的 CMT/XA 或 IBM MQ 重送行為。
先找到每一段由誰接手
IBM MQ → Jakarta resource adapter → EAP EJB container
→ MDB.onMessage → managed service → enlisted DB resource
Spring bean proxy and EJB container interception are different mechanisms.MDB 的生命週期由 EJB 容器管理。它呼叫的 Service 若是 EJB,要查 EJB 的交易屬性;若是 Spring Bean,要查怎麼取得 Bean、選中哪個管理器,以及資源有沒有加入預期交易。直接 new 一個帶 @Transactional 的 Spring 物件,不能讓 EAP 的交易自動接上它;第九課的手動建立反例已示範 annotation 本身不夠。
Jakarta Enterprise Beans 對 container-managed MDB message listener 的 transaction attribute 限定為 REQUIRED 或 NOT_SUPPORTED。這是在說規範允許的選項,實際設定、descriptor 覆寫、adapter 能力與資源參與仍要核對。Spring 的 annotation 與 EJB 的交易屬性不能只因名字相似就互換。
兩段提交和同一筆 XA,不能混為一談
Local model in this kit:
DB transaction commits → model records acknowledgment
↑ failure window
Target CMT/XA investigation:
transaction coordinator → enlisted MQ + DB resources
verify actual outcome and recovery, not this model alone教材刻意選兩段式的本機模型,讓「DB 已提交、確認遺失」的窗口看得見。若你的 MDB、MQ 與 DB 確實參與同一筆容器協調交易,應依那個模型驗證提交與復原,不能直接套用這裡的預期。也不要為了做練習,把原本企業系統的交易架構改成這一套。
同一筆本機交易寫兩張表
return transaction.execute(status -> {
// First detect an existing business key and compare its payload.
jdbc.update("INSERT INTO processed_events(business_key,title) VALUES (?,?)", key, title);
jdbc.update("INSERT INTO orders(business_key,title) VALUES (?,?)", key, title);
if (failBeforeCommit) throw new IllegalStateException("synthetic failure");
return Result.APPLIED;
});這是 OrderProcessor 的核心節錄,完整程式還包含輸入檢查與重複判斷。processed_events 記錄已處理的業務鍵,orders 是業務資料,兩者在同一筆 H2 交易內寫入。若中途失敗,兩張表都回到零筆;不然只留下已處理標記,下一次可能誤以為工作早就完成。
再次收到相同業務鍵時,範例先比較 title;相同才回 DUPLICATE,內容不同就報衝突。這是順序執行的教學實作,SELECT 後 INSERT 並不是完整的併發去重演算法。雖然 primary key 防止重複列,競爭失敗的處理、重試、隔離等級與資料保留期限都還沒驗證。
跑六個案例,觀察失敗位置
mvn test
mvn package| 情境 | 資料庫 | 模型結果 |
|---|---|---|
| 正常處理 | 訂單與標記各一筆 | 已確認 |
| 提交前拋錯,再重送 | 先各零筆,再各一筆 | 第二次成功 |
| 已提交,但未記錄確認 | 維持各一筆 | 重送判為 DUPLICATE |
| 同業務鍵、內容不同 | 原資料保留 | 報衝突 |
| 連續三次合成失敗 | 各零筆 | 標為隔離 |
| 缺業務鍵 | 各零筆 | 拒絕輸入 |
第三個案例會建立新的 OrderProcessor,再對同一份 H2 處理一次,確認判斷依據來自資料庫,不是 Service 物件的記憶。但兩次仍在同一個 JVM、同一個記憶體資料庫;沒有真的殺程序,也沒有驗證斷電後的耐久性或 XA recovery。
Poison message 不是永遠多試一次
第五個案例的三次重試是刻意固定的教學條件,quarantined 也只是一個 boolean。IBM MQ 文件另外說明 backout count、BOTHRESH 與 BOQNAME 等機制;實際 adapter、目的地權限、無法搬移訊息時的行為,都必須回到目標組合查證。不能把模型裡第三次停止,抄成正式站應使用的設定。
如果決定把失敗訊息移到別處,還要保留業務鍵、原始內容、錯誤分類與可追蹤的重送方式。只是清掉 queue 或吞掉例外,可能讓「看不到訊息」冒充「業務已完成」。容器已標記回滾的交易,也不會因為應用 catch 後寫一行 log 就被修復。
把本機結果接回公司的驗收案例
下載包的 TARGET-ENVIRONMENT.md 已列出要填的 adapter、destination、交易管理器、資源參與與八種目標案例。真正執行時保留 DB 前後狀態、MQ 業務鍵與次數、交易和錯誤 log,再交給第七課的比對工具;不要把這裡的合成結果填成公司環境的實測資料。
自我檢查:DB 一筆、queue 沒訊息,能證明剛好處理一次嗎?不能。你還缺投遞、消費、業務副作用與復原的完整關聯。這個範例能確定的是六個本機情境下的 H2 結果;它把下一步需要觀察的位置指出來,並沒有替真實容器回答。
回顧第 09 課:先辨認 Service 的代理與交易第 07 課:用同一案例比對四種證據