1. 題目
一個訂單事件流經過解析、視窗聚合與風控計算,結果寫入倉庫並觸發下游通知。任務會因 worker 崩潰、網路逾時與遲到事件重試。請解釋 exactly-once 的邊界,並設計不會因重試產生重複帳單的方案。
2. 約束與澄清
- 先區分三層語義:訊息投遞、管線內處理結果、外部系統副作用。
- 事件可能重複、亂序或遲到;不能假設日誌中的「處理過」就是已提交。
- 結果需要可重播,通知等外部呼叫必須具備冪等鍵或去重能力。
- 先明確允許的延遲、遲到視窗與丟棄策略,再討論實作。
3. 核心概念
Exactly-once processing 通常表示每筆記錄的管線結果在持久化輸出中最多反映一次,同時系統仍要保證記錄不遺失;它不等於每段使用者程式只執行一次,也不會自動涵蓋外部 HTTP、郵件或資料庫呼叫。At-least-once 輸入加上檢查點、確定性重播與結果去重,才能形成可驗證的輸出保證。
4. 參考流程
onEvent(event):
key = stableEventId(event)
state = readCheckpointOrState(key)
result = deterministicTransform(event, state)
writeTransactionalResult(key, result) # unique(key)
commitCheckpointAfterResult(key)
onExternalSideEffect(result):
idempotencyKey = result.eventId + ":" + result.version
callOrOutbox(idempotencyKey, result.payload)管線內先把結果與事件 ID 寫入支援唯一約束或交易提交的儲存,再推進檢查點。外部通知透過冪等介面或 outbox 交給獨立發送器;發送器可以重複執行,但接收方只接受同一個冪等鍵一次。
5. 失敗場景與取捨
worker 在外部呼叫成功後、檢查點提交前崩潰,重播會再次呼叫外部服務;沒有冪等鍵時,不能只靠 runner 消除副作用。視窗結果還會受遲到資料與 watermark 影響,必須定義允許更正的範圍。更強的端到端保證通常增加去重狀態、交易協調與儲存成本;若業務允許重複,可選擇 at-least-once 來換取較低延遲。
6. 驗證與觀測
- 注入崩潰、逾時、重複訊息與亂序事件,檢查同一業務鍵的最終結果。
- 記錄輸入事件 ID、處理嘗試次數、提交版本、去重命中與外部呼叫結果。
- 對帳「收到、處理、提交、通知」四個計數,不能只看 worker 日誌。
- 監控重複率、遲到率、檢查點年齡、去重狀態大小與重播積壓。
7. 常見誤區
- 把 exactly-once delivery、exactly-once processing 與 exactly-once side effect 當成同一個承諾。
- 以為開啟框架開關後,任意自訂程式與外部 API 都自動獲得一次性效果。
- 使用時間戳而非穩定事件 ID 去重,導致重試或批次重播產生不同鍵。
- 忽略遲到事件、版本衝突與去重記錄的保留期限。
8. 面試評分點
能劃清保證邊界
應分別說明投遞、管線結果與外部副作用,並指出框架保證通常只涵蓋其中一部分。
能設計可重播流程
應使用穩定事件 ID、確定性轉換、交易結果提交與檢查點順序,解釋崩潰後的重播路徑。
能處理外部副作用
應提出冪等鍵、唯一約束或 outbox,並說明接收方與發送器如何共同防止重複。
能用故障注入驗證
應涵蓋重複、亂序、遲到、逾時與 worker 崩潰,使用提交資料與業務對帳驗證結論。