REGULAR PURCHASE NOTIFICATION

購買會自動發生,重要的事也要主動告知

定期購只需設定一次,之後便依週期自動建立訂單。消費者不會每次親自操作,因此需要通知,才能掌握進度與異常。

在適當的時間,把正確的定期購資訊,可靠地通知需要知道的人。
01
發生什麼事,要讓消費者知道什麼?
定期購事件
通知的目的
即將建立訂單
事先準備
確認是否需要調整。
商品庫存不足
知道購買可能受影響
這一期可能無法正常購買。
付款或建立訂單失敗
知道這次未完成
可能需要採取行動。
02
通知需要做到哪些事?
需求要解決的問題
對象與內容正確真的發生這件事,才通知對應的人。
及時通知需要事先知道的事,不能太晚收到。
不漏送通知量再多,也不能只送出一部分。
失敗能補送短暫的系統或網路問題,不應讓通知消失。
避免重複系統重新執行,不應一直寄相同內容。
處理過時通知問題已解決或時機已過,需決定是否補送。
結果可追查客服與維運能查到是否發送、時間與失敗原因。
03
同一件事重送,與下一期通知有何不同?

以「庫存不足通知」為例,是否需要再次通知,不能只看通知類型,還要看它是不是同一個業務事件。

本期
第一次發現商品庫存不足,通知消費者
需要通知
仍是本期
系統重新執行,同一件事又準備寄一次
應避免重複
下一期
新的購買再次遇到庫存不足
可能是新的必要通知
QUEUE AS A DURABLE TODO LIST

需要的不只是發送紀錄,而是一份可持續處理的通知待辦清單

通知資料不只回答之前做過什麼,也要讓發送程式知道還有哪些工作尚未完成。

01
通知資料如何同時成為紀錄與待辦?
觸發
業務事件
庫存不足、建單前提醒或處理失敗
→判斷
處理
通知 Job
找到對象,組成通知內容
→寫入
保存與待辦
notify_queue
保存內容、狀態與嘗試結果,提供待發與失敗通知
→發送
下游
SCM 通知 API
接收通知並回傳本次處理結果

notify_queue 位在業務判斷與外部發送之間。如果只是保存發送結果,稱為「通知紀錄表」就足夠;它之所以具有 Queue 的角色,是因為它也參與工作的執行。

回顧過去
→
通知處理紀錄:保存之前送了什麼,以及成功或失敗。
推進未來
→
通知待辦清單:保存尚未完成的通知,讓原通知 Job 與 Retry Job 持續取出處理。
02
什麼情況需要在意通知順序?

處理順序可能依最早、最急或已到發送時間等需求決定,但不能從 Queue 這個名稱直接推定必須先進先出。

準備發送一筆通知
是否屬於同一筆定期購?
否
不同消費者之間
通常不需要嚴格保證誰先收到。
是
同一筆定期購之內
檢查後來狀態是否讓舊通知失效,避免恢復後才收到過時的失敗通知。
先取出不等於先收到。中途重試或下游發送延遲,都可能改變收件順序;是否必須先進先出,需要獨立確認。
TWO QUEUES, TWO WORK UNITS

NMQ 安排 Job,notify_queue 管理每一筆通知

公司已有共用的 Message Queue 系統,但中央系統不容易直接呈現定期購服務內每一筆通知的狀態,因此仍需要自己的通知佇列表。

01
從排程到發送,用四個檢查點定位問題
檢查點 01
NMQ
Job 是否成功排入,或仍在中央 Queue 等待
→安排執行
檢查點 02
通知 Job
是否已執行、找到對象並準備通知
→建立待辦
檢查點 03
notify_queue
通知是否產生、目前狀態與補送依據
→呼叫 API
檢查點 04
SCM 通知 API
是否接受通知並回傳本次處理結果

NMQ 負責安排 Job 執行,但定期購對中央排隊過程的可見度有限。自己的通知表從「通知已產生」開始,保存可查詢與可補送的處理進度。

比較面向NMQnotify_queue 表
工作單位一次 Job 任務一筆通知
例如執行一次庫存通知 Job寄給某位消費者的庫存不足通知
關心的進度Job 是否執行完成各筆通知是否發送成功
重試的範圍重新執行 Job補送符合條件的個別通知
ONE PURPOSE PER JOB

不同通知拆成不同 Job,讓時間、條件與內容各自清楚

庫存提醒、建立訂單前提醒與失敗通知不是同一種工作。它們需要不同排程、不同查詢條件,也產生不同內容。

◷ 執行時機◇ 判斷條件→ 通知內容

有些通知需要事前提醒,有些只能在失敗發生後建立。每種通知查詢的資料與業務條件不同,通知對象、內容與下游 API 也需要依目的分開組裝,不應藏在同一支大型 Job 裡。

NMQ 排程與事件觸發
庫存通知 Job
◷ 未來庫存檢查◇ 哪些商品庫存不足→ StockNotify
建單前通知 Job
◷ 下一期訂單建立前◇ 哪些定期購即將建單→ 建單前通知
失敗通知 Job
◷ 付款或建單失敗後◇ 哪些失敗需要通知→ 失敗通知
Retry Job
↻ 後續補送◇ 只處理既有失敗通知→ 不建立新 Queue
各自獨立後,每支 Job 的業務目的更清楚,也能分別安排排程、調整條件和追查問題。看到 RegularOrderStockNotifyJob,便能直接辨識這次執行要處理庫存通知。
DUPLICATION AND CONCURRENCY

重複建立、重複發送與同時處理,是三個不同問題

同一支 Job 再次執行、SCM 已接收但狀態尚未回寫,以及兩支 Job 同時撈取,都可能造成重複,但需要在不同邊界處理。

01
同一個 Job 重做,如何避免重複建立?

在寫入 notify_queue 前,需要先用一組能代表業務事件的條件查詢。查到相同事件已建立時,就略過這筆,而不是只記錄「今天 Job 執行過」。如果只記錄整支 Job 已執行,後面 70 筆通知可能一起被跳過。

Job 找到通知對象,組成業務識別條件
相同業務事件已建立?
是
略過或沿用既有 Queue
不再建立相同通知。
否
建立新的 Queue
保存這次業務事件的通知。
Job建立的通知類型目前用來判斷已建立的條件查到後的處理
RegularOrderStockNotifyJobStockNotify商店 ID + 通知類型跳過該商店,不新增。
RegularCreateOrderNotifyJobCreditCardOrderNotify、CashOnDeliveryOrderNotify定期購訂單 ID + 通知類型跳過該筆通知,不新增。
RegularOrderFailedNotifyJobCreditCardFailedNotify、CreateFailedNotify定期購訂單 ID + 通知類型跳過該筆通知,不新增。
RegularOrderUnsubscribeNotifyJobUnsubscribeNotify商店 ID +訂單 ID +通知類型沿用既有 Queue,不新增。
RegularNotifyQueueRetryJob處理既有通知不適用,不負責新增 Queue。撈取既有 Queue 發送並更新狀態。
業務日期必須跟著通知事件。例如 10/9 補跑 10/8 的任務,仍應判斷 10/8 的通知是否存在。現有 Job 條件表沒有日期欄位,因此是否需要把業務日期納入識別鍵,仍要依同一筆 reference 在不同週期是否需要再次通知來確認。
02
SCM 已接受,但成功狀態尚未回寫

目前架構文件使用 nq_send_status 表示通知處理進度,原通知 Job 與 Retry Job 依狀態分工。

0|待發
由原通知 Job 處理。
2|失敗
由 Retry Job 處理。
1|成功
SCM 通知 API 回報成功,不代表收件者已實際收到。
SCM 接受通知後的正常與中斷分支時序圖 通知 Job 讀取 Queue 並呼叫 SCM。正常分支會回寫成功狀態,下次不再撈取;中斷分支在回寫前停止,Queue 仍是未完成狀態,下次可能使用相同識別碼再次發送。 通知 Job notify_queue SCM 通知 API 讀取 status 0 或 2 回傳 Queue 與固定識別碼 A 發送 A 與通知內容 SCM 接受通知 正常完成 回寫 status = 1 下次不再撈取 回寫前中斷 Queue 仍是 status 0 或 2,SCM 卻可能已建立通知 下次再次讀到未完成狀態 使用同一個識別碼 A 再送一次 SCM 也要記錄並辨認 A,才能在下游防重
時間由上往下。SCM 接受與本地回寫是兩個動作,中間的中斷空隙無法只靠本地狀態完全消除。
跨系統冪等提案:可以評估使用 Queue PK 作為固定發送識別碼,但 SCM 必須支援以此識別碼防重,而且識別碼的範圍與保存時間要足夠。單純傳送 Queue PK 不會自動產生冪等效果。
03
兩個 Job 同時處理同一筆 Queue

如果兩個 Job 都可能處理同一筆 Queue,就需要「同一時間只允許其中一個取得處理權」的機制。重點是,查到可以處理與取得處理權必須一起完成,不能兩邊都先查到資料,準備發送時才各自標記。

比較面向整批鎖逐筆領取
鎖定粒度以通知類型為單位,例如 StockNotify。以單筆 Queue 為單位。
可否平行同類型一次只由一個 Job 處理。多個 Job 可同時處理不同 Queue。
實作方式先取得跨 Pod 的類型鎖,再撈取、發送與回寫。由資料庫原子地領取尚未被其他執行者取得的 Queue。
適合情境希望改動較少,先避免同類處理範圍重疊。需要平行發送並提高整體處理量。
通知類型整批鎖設計提案 Job A 先取得 StockNotify 的跨 Pod 鎖。Job B 同時嘗試取得相同鎖時失敗,因此略過本輪。Job A 在鎖的範圍內完成撈取 Queue、發送與回寫,最後釋放鎖。 整批鎖設計提案 Job A 跨 Pod 鎖 Job B 取得 StockNotify 鎖 嘗試取得相同鎖 取得成功 未取得,本輪略過 鎖定範圍 撈取 Queue → 發送 → 回寫處理結果 完成後釋放鎖 同類型的處理範圍不會重疊
設計邊界:鎖必須涵蓋「撈取、發送、回寫」,且所有 Job 都遵守同一規則。鎖也必須能跨 Pod 共用,例如 PostgreSQL advisory lock;C# lock 無法協調不同 Pod。這是待評估的設計提案,不代表現況已採用。
問題需要保護的邊界
重複建立 Queue業務事件識別條件,必要時搭配唯一約束。
SCM 已接受後重送下游也支援固定發送識別碼與冪等處理。
多個 Job 同時處理跨 Pod 的分散式鎖,或資料庫原子領取。