定期購通知 Queue 設計:從雙層佇列到防重與併發控制
購買會自動發生,重要的事也要主動告知
定期購只需設定一次,之後便依週期自動建立訂單。消費者不會每次親自操作,因此需要通知,才能掌握進度與異常。
| 需求 | 要解決的問題 |
|---|---|
| 對象與內容正確 | 真的發生這件事,才通知對應的人。 |
| 及時通知 | 需要事先知道的事,不能太晚收到。 |
| 不漏送 | 通知量再多,也不能只送出一部分。 |
| 失敗能補送 | 短暫的系統或網路問題,不應讓通知消失。 |
| 避免重複 | 系統重新執行,不應一直寄相同內容。 |
| 處理過時通知 | 問題已解決或時機已過,需決定是否補送。 |
| 結果可追查 | 客服與維運能查到是否發送、時間與失敗原因。 |
以「庫存不足通知」為例,是否需要再次通知,不能只看通知類型,還要看它是不是同一個業務事件。
需要的不只是發送紀錄,而是一份可持續處理的通知待辦清單
通知資料不只回答之前做過什麼,也要讓發送程式知道還有哪些工作尚未完成。
notify_queuenotify_queue 位在業務判斷與外部發送之間。如果只是保存發送結果,稱為「通知紀錄表」就足夠;它之所以具有 Queue 的角色,是因為它也參與工作的執行。
處理順序可能依最早、最急或已到發送時間等需求決定,但不能從 Queue 這個名稱直接推定必須先進先出。
NMQ 安排 Job,notify_queue 管理每一筆通知
公司已有共用的 Message Queue 系統,但中央系統不容易直接呈現定期購服務內每一筆通知的狀態,因此仍需要自己的通知佇列表。
notify_queueNMQ 負責安排 Job 執行,但定期購對中央排隊過程的可見度有限。自己的通知表從「通知已產生」開始,保存可查詢與可補送的處理進度。
| 比較面向 | NMQ | notify_queue 表 |
|---|---|---|
| 工作單位 | 一次 Job 任務 | 一筆通知 |
| 例如 | 執行一次庫存通知 Job | 寄給某位消費者的庫存不足通知 |
| 關心的進度 | Job 是否執行完成 | 各筆通知是否發送成功 |
| 重試的範圍 | 重新執行 Job | 補送符合條件的個別通知 |
不同通知拆成不同 Job,讓時間、條件與內容各自清楚
庫存提醒、建立訂單前提醒與失敗通知不是同一種工作。它們需要不同排程、不同查詢條件,也產生不同內容。
有些通知需要事前提醒,有些只能在失敗發生後建立。每種通知查詢的資料與業務條件不同,通知對象、內容與下游 API 也需要依目的分開組裝,不應藏在同一支大型 Job 裡。
RegularOrderStockNotifyJob,便能直接辨識這次執行要處理庫存通知。重複建立、重複發送與同時處理,是三個不同問題
同一支 Job 再次執行、SCM 已接收但狀態尚未回寫,以及兩支 Job 同時撈取,都可能造成重複,但需要在不同邊界處理。
在寫入 notify_queue 前,需要先用一組能代表業務事件的條件查詢。查到相同事件已建立時,就略過這筆,而不是只記錄「今天 Job 執行過」。如果只記錄整支 Job 已執行,後面 70 筆通知可能一起被跳過。
| Job | 建立的通知類型 | 目前用來判斷已建立的條件 | 查到後的處理 |
|---|---|---|---|
RegularOrderStockNotifyJob | StockNotify | 商店 ID + 通知類型 | 跳過該商店,不新增。 |
RegularCreateOrderNotifyJob | CreditCardOrderNotify、CashOnDeliveryOrderNotify | 定期購訂單 ID + 通知類型 | 跳過該筆通知,不新增。 |
RegularOrderFailedNotifyJob | CreditCardFailedNotify、CreateFailedNotify | 定期購訂單 ID + 通知類型 | 跳過該筆通知,不新增。 |
RegularOrderUnsubscribeNotifyJob | UnsubscribeNotify | 商店 ID +訂單 ID +通知類型 | 沿用既有 Queue,不新增。 |
RegularNotifyQueueRetryJob | 處理既有通知 | 不適用,不負責新增 Queue。 | 撈取既有 Queue 發送並更新狀態。 |
目前架構文件使用 nq_send_status 表示通知處理進度,原通知 Job 與 Retry Job 依狀態分工。
如果兩個 Job 都可能處理同一筆 Queue,就需要「同一時間只允許其中一個取得處理權」的機制。重點是,查到可以處理與取得處理權必須一起完成,不能兩邊都先查到資料,準備發送時才各自標記。
| 比較面向 | 整批鎖 | 逐筆領取 |
|---|---|---|
| 鎖定粒度 | 以通知類型為單位,例如 StockNotify。 | 以單筆 Queue 為單位。 |
| 可否平行 | 同類型一次只由一個 Job 處理。 | 多個 Job 可同時處理不同 Queue。 |
| 實作方式 | 先取得跨 Pod 的類型鎖,再撈取、發送與回寫。 | 由資料庫原子地領取尚未被其他執行者取得的 Queue。 |
| 適合情境 | 希望改動較少,先避免同類處理範圍重疊。 | 需要平行發送並提高整體處理量。 |
lock 無法協調不同 Pod。這是待評估的設計提案,不代表現況已採用。| 問題 | 需要保護的邊界 |
|---|---|
| 重複建立 Queue | 業務事件識別條件,必要時搭配唯一約束。 |
| SCM 已接受後重送 | 下游也支援固定發送識別碼與冪等處理。 |
| 多個 Job 同時處理 | 跨 Pod 的分散式鎖,或資料庫原子領取。 |


