可靠的非同步流程,需要讓每次交接都有可恢復的持久狀態,也需要限制重送與舊處理者能造成的影響。 本篇把文章匯出服務從單一工作逐步擴充為通知、搜尋與統計各自消費的流程,並用故障窗口檢查承諾。
本文回收前面 01–11 的責任、確認、順序、恢復與契約;不要求先部署任何指定產品。以下所有表格、狀態與識別都是自創設計,查證日期為 2026-11-04;依前文官方來源界定機制,沒有實作、壓測或故障實驗結果。
先寫需求與模型
本例有四項需求:使用者能安全重送建立匯出的同一意圖;每個成功工作有一份被正式選定的結果;完成事實讓各下游獨立處理;程序停止後能找回待做與未知工作。
「一份選定結果」不表示檔案儲存體裡永遠只產生一個物件。處理嘗試可能重複,暫存產物可以有多份;對外以工作資料列選定的結果為準。也不承諾所有通知恰好送到使用者一次。
案例採用以下前提,離開前提便要重新驗證:
- 工作、意圖對應及匯出端 Outbox 位於同一個支援交易、唯一約束與條件更新的資料庫;已確認提交在本例程序重啟後仍保存。
- 檔案儲存允許每次嘗試使用不同鍵,已完成產物按契約持久保存,且舊嘗試不能覆寫已選定物件。資料庫中的結果參照仍須按工作授權讀取。
- broker 支援所選的保存、確認與重投機制;各獨立消費責任有自己的進度。保存期、權限與容量都已按恢復需求設定,並不是無限保留。
- 搜尋投影與統計各使用能把去重紀錄和更新放在同一本地交易的儲存;通知供應者屬外部系統,另訂冪等、查詢與未知結果契約。
這些是設計條件,沒有宣稱任意 SQL、搜尋引擎、物件儲存與通知 API 都天然符合。
第一步:受理與待交付工作一起保存
呼叫端帶 export-intent-7,API 驗證身分、輸入與配額。本例在一筆短交易內保存「呼叫者+操作+意圖識別」的唯一對應、job-42 的 pending 狀態,以及指向此工作的 cmd-42 待發紀錄。重送同意圖同參數取得同工作,同識別不同參數則拒絕。
交易提交後才回應受理;回應遺失時仍用原意圖核對。202 本身不提供上述持久性,這是本例服務承諾;對外契約見同步呼叫與非同步工作邊界。
relay 只讀已提交的待發紀錄,取得符合需求的 broker 發布證據後才保存發布進度。確認遺失時可以重送 cmd-42,不另建工作。不能先標記已發再做發布,也不能把未路由的 confirm 當成目標 queue 已收到。
這是 Outbox 的本地原子邊界:工作與待發紀錄一起存在或一起回復;relay 仍可能重送。參考 AWS:Transactional outbox,Issues and considerations、Using an outbox table。具體並行轉送與確認界線見資料庫與訊息的原子缺口。
第二步:worker 可以重做,但只能有合法的完成寫入
job-42 的概念狀態如下。對外終態與內部嘗試分開,單次暫時錯誤不立即成為永久失敗:
| 目前狀態 | 合法推進及本例條件 | 持久紀錄 |
|---|---|---|
pending | 原子取得一次處理資格,進入 running | 處理世代、嘗試識別與恢復期限 |
running | 暫時失敗可安排再試;超過恢復期限可由恢復者作條件式接管 | 新世代或持久重試安排;期限到只代表可懷疑舊嘗試 |
running | 當前世代保存完整產物後,條件式提交成功 | 結果識別、狀態版本與完成事件 Outbox |
pending/running | 確定無法履行且按契約終止 | failed、可解釋原因與仍需清理的產物 |
succeeded/failed | 同一命令再到達只核對既有終態 | 不重新開啟工作;新的業務意圖另建工作 |
恢復期限不是遠端停止命令。A 暫停後由 B 接手,A 仍可能恢復;接管交易須更新處理世代,A 的完成交易也必須在同一個資料庫判斷與更新邊界驗證自己仍是目前世代、工作仍在允許完成的狀態。不能只在開始產生檔案前查一次。
本例流程是:取得資格、寫入以嘗試識別區分且不可覆寫的候選檔案、確認候選產物完整,再以目前世代作條件更新;該交易同時選定結果、設為 succeeded,並新增唯一的完成事實 evt-90。條件不符便不發布新的完成事實。提交結果未知時,先查工作與事件紀錄,不憑斷線判定失敗。
若 A 的交易被拒,A 已產生的候選檔案可能成為孤兒產物。本例允許它,並由清理程序在確認沒有有效執行或工作參照後回收;不能在某個任意 timeout 到期就刪除可能正在完成的結果。這個做法保護選定結果,不是將檔案寫入加入資料庫交易。原理見消費並行、分區與順序及資料庫與訊息的原子缺口。
worker 在業務狀態可靠保存後,才依消費契約結束投遞。若工作尚由有效嘗試執行,不可把收到重複命令直接當成業務完成;須延後投遞或依已持久保存的恢復安排處理。即使原投遞已結束,本例恢復程序仍會掃描逾期的非終態工作,作條件式接管並補交命令,避免只靠某次 ack 維持活性。
第三步:完成事件有自己的身分與契約
evt-90 表達「job-42 已選定可用結果」,不是「所有下游完成」。事件攜帶來源、類型、工作識別、結果識別與業務版本;不散播完整檔案或可長期繞過授權的網址。
若採 CloudEvents,原自訂 event_id 對應到 id,事件來源使用約定的 URI-reference;source + id 識別同一事實。新舊封套交疊時,沿用 08 明訂的來源映射,在 Inbox 檢查前正規化同一事件鍵並涵蓋既有去重紀錄,避免把改封套當成新事實。relay 重送保留這組身分,重新排隊的傳輸識別則可以改變。specversion 不能代替業務資料版本。參考 CloudEvents 1.0.2:Required Attributes與事件封套、Schema 與相容演進。
第四步:三個下游各自前進
完成事件路由到獨立的通知、搜尋與統計訂閱;同一訂閱中的 worker 才分攤相同責任。過濾只減少符合條件的傳遞,不取代發布、訂閱與結果讀取的授權。各下游的恢復與容量安排見訂閱拓樸、路由與隔離。
| 下游 | 本例本地交易可保證的邊界 | 邊界之外仍要處理的事 |
|---|---|---|
| 搜尋投影 | 在同一交易檢查 consumer/投影世代及事件去重,條件式更新投影版本 | 若改用另一個外部搜尋引擎,必須重新設計索引更新的交接與冪等 |
| 統計 | 唯一事件去重與統計增量一起提交;重送不再次累加 | 去重保留不足或歷史漏失,不能靠重新讀一次自動補齊 |
| 通知 | 去重與「待送通知意圖」一起保存 | 供應者是否接受、是否真正送達,以及不確定結果 |
通知 dispatcher 使用穩定的通知意圖識別,在供應者明訂的冪等範圍與期限內重送或查詢。若外部呼叫已成功但回應遺失,而對方沒有可用冪等/查詢契約,狀態就保留 unknown 交給核對流程。不能因本地沒有成功紀錄而無條件再寄,也不能因供應者接受而直接記為使用者已閱讀。
第五步:重播只進入允許的效果路徑
搜尋重建使用新的投影世代、獨立讀取位置與去重範圍,先回填仍保存的歷史,再追趕到定義的切換邊界;處理版本、刪除與即時事件後,才切換查詢入口。不能清掉既有 Inbox 就讓所有 consumer 一起重播。
歷史回放路徑不連到通知 dispatcher;只有明確的新通知意圖才可進入對外寄送。權限、路由與程式處理都應落實這個限制,不能只加一個沒有人檢查的 replay 欄位。沒有完整歷史時,改以可信快照加後續變更重建,或承認無法重建;不可把 Kafka compaction 當成所有事實永遠存在。完整限制見重播、投影與歷史資料。
用一張故障矩陣檢查恢復
| 故障點 | 持久證據與恢復動作 | 仍不保證的事項 |
|---|---|---|
| 受理提交後回應遺失 | 以原意圖核對 job-42 | 保留期外的舊鍵需依業務規則處理 |
| relay 發出後、記進度前停止 | 待發紀錄仍在,恢復後用同一應用身分重送 | broker 不因此免除重投 |
| 檔案完整但成功交易未提交 | 依工作世代核對/接管;候選檔案可能需清理 | 不宣稱物理檔案只寫一次 |
| B 接管後 A 恢復 | A 的資料庫完成更新因舊世代被拒;A 不能覆寫 B 物件 | 不合作的外部服務不會自動被資料庫世代阻擋 |
| consumer 效果提交後尚未 ack | 再投遞時查本地去重紀錄 | 外部效果不在該本地交易內 |
| 通知呼叫逾時,結果未知 | 按供應者契約查詢/同鍵重試,否則保留未知並核對 | 不能以逾時證明沒有寄出 |
| 壞事件進入死信 | 保存原身分、錯誤與處置責任,小量驗證後限速重送 | 移入或重送都不證明效果未曾發生 |
以上矩陣是設計推演,不是已執行測試。若要驗證實作,須按積壓、觀測與故障驗證記錄各次故障、狀態與反例,檢查受理到終態、Outbox 年齡、獨立下游進度與未知效果,而不是只看 queue 是否清空。
最後回查承諾與代價
在本例交易、唯一性、授權、持久性與恢復前提成立時,可以主張同一有效意圖對應同一工作,只有合法世代能選定結果,且本地去重交易保護其涵蓋的效果。要主張最終前進,還需要依賴恢復、足夠資源、持續重試與有人處理隔離工作;不能由安全性條件推論一定及時完成。
代價是持久紀錄、候選產物清理、去重保留、Schema 治理及故障處置。需求若只有少量背景匯出,可以停在單資料庫工作掃描;真的需要多個獨立下游或歷史重建時,再增加對應的訂閱與保存機制。完整設計的價值在於每個機制都有明確責任,且承諾可以沿實際歷程被檢查。
相關文件
- 訊息佇列與事件驅動架構知識索引:回查十二篇的問題與閱讀順序。
- 從重送請求到冪等副作用:本地原子邊界與保留期。
- 長時間執行操作與非同步 API 契約:對外工作狀態與結果查詢。