cover

外部金流回呼打進來,你的服務當場開始算帳、寫三張表、發兩封信。做完之前,對方的逾時先到了。

它看到非 200,於是重送。你的 worker 還在處理上一筆,新的又進來。雪球從這裡開始滾:處理不完 → 超時 → 重送 → 負載更重 → 更處理不完。

擋住它只要一個動作:收下、丟進佇列、立刻回 200。真正的工作交給後面慢慢做。

這篇講在 AWS 上這個佇列該挑哪一個,以及挑完之後那兩件不做就等於沒做的事。

原理那半——為什麼要事件驅動、佇列跟串流差在哪——在 Event-driven Queue,這篇不重寫。

選型只要問四個問題

  1. 掉了會出事嗎? 會 → 不能用 Redis pub/sub
  2. 一則訊息要給幾個人? 一個 → SQS;多個 → SNS 或 EventBridge 扇出
  3. 需要重播歷史嗎? 需要 → Kinesis 或 MSK
  4. 順序重要嗎? 重要 → SQS FIFO

第一題是最容易錯的那題,所以先說清楚。

Redis pub/sub 是廣播,不是佇列。 訂閱者當下不在線,那則訊息就永遠消失了——沒有重送、沒有確認、沒有歷史。它跟 SNS 名字像、形狀像,但 SNS 有重試和死信佇列,Redis pub/sub 什麼都沒有。

把「通知會員扣款成功」這種事丟 Redis pub/sub,worker 剛好在重啟,那則通知就沒了,而且你不會知道。

選項對照

服務一句話什麼時候用
SQS佇列,一則訊息一個消費者拿走預設先選這個。背景工作、webhook 緩衝
SQS FIFO同上,但保證順序、不重複順序影響正確性時,例如同一筆訂單的狀態變更
SNS發佈訂閱,一則複製給多個訂閱者一個事件要通知好幾個下游
EventBridge事件匯流排,可依內容過濾、可排程、可接外部 SaaS跨服務的領域事件
Kinesis / MSK高吞吐串流,可重播真的要重播,或每秒幾萬筆才碰
Redis Streams輕量佇列,有消費者群組、有確認已經有 Redis、規模不大
Redis Pub/Sub廣播,會掉只用在「掉了也沒差」:快取失效通知、WebSocket 推播

Kinesis 那一列要特別克制。每秒幾萬筆以下、不需要重播的場景選它就是過度設計。選了它,你會換來一整套運維,去解一個你沒有的問題:分片管理(資料流切成幾段平行處理,切幾段要自己管)、消費者位移(每個消費者讀到哪一筆,要記下來)、重新平衡(消費者增減時,重新分配誰讀哪一段)。

為什麼是三個服務不是一個

這三個不是同代的替代品,是每一代撞牆之後長出來的。

SQS 先解「同步做不完」。 一則訊息一個消費者拿走,工作被推到後面去,前面就不會塞住。

但 SQS 撞的牆是一則只能給一個人。訂單成立要同時通知點數、會員、推播,你得在程式裡發三次,而且每加一個下游就要改一次發送端的 code。

SNS 解這面牆:發一則,複製給所有訂閱者。發送端不用認識下游。

SNS 自己的牆是只能照 topic 分流。下游說「我只要金額大於一萬的訂單」,SNS 給不了,它只能把全部推過去讓你自己濾——那些不該來的訊息還是消耗了你的 worker。

EventBridge 解這面:規則可以看訊息內容過濾,還附帶排程和外部 SaaS 接點。

EventBridge 也有牆,而且它是本段裡唯一還沒被解掉的:它不保存歷史。新的訂閱者加進來,只看得到從現在開始的事件,之前的看不到。要重播就得往 Kinesis 或 Kafka 走,代價是前面講的那整套運維。

四個落點

以「ALB + ECS + 外部金流」這類架構為例,非同步該出現在這四個地方。

webhook 收到就丟佇列

外部回呼 → ALB → 服務收下 → 丟 SQS → 立刻回 200
                                   ↓
                              worker 非同步處理

開頭那顆雪球就是這樣擋掉的。這是四個落點裡最該先做的一個。

把排程從單台機器解耦

很多架構裡有一台 EC2 跑 cron。那是單點——掛了沒人接手,也擴不了。

EventBridge 定時 → 丟訊息進 SQS → ECS worker 消化

搬之前先處理那台機器上的本地狀態,不然搬完會發現 worker 讀不到檔案。

跨服務的領域事件

訂單成立要通知點數、會員、推播。同步一個一個呼叫的話,服務之間就得互相認識——在 ECS 上還得繞 ALB 才找得到對方。

改成發一個事件、各自訂閱,耦合就降下來了。這是 EventBridge 的主場。

快取失效廣播

這個剛好適合 Redis pub/sub。掉了下次讀自然重建,沒差。

前面說 Redis pub/sub 會掉是缺點,在這裡它是可以接受的代價——差別在於這件事掉了不會有人受傷。

兩個配套,不做等於白做

死信佇列

處理失敗超過 N 次就丟進去的另一個佇列。SQS 內建,設定就好。

沒有它,壞訊息會無限重試,把 worker 佔滿。 一則永遠解析失敗的訊息可以讓整條佇列停擺。

死信佇列只要有東西就該告警——那裡本來就不該有東西。這跟一般的「超過門檻才告警」不同,門檻是零。

冪等:做幾次結果都一樣

SQS 是「至少送一次」。重複是正常現象,不是故障。

這句話在金流場景的意思是:沒做冪等就會重複扣款。不是可能會,是遲早會。

實作上就是拿訊息 ID 或訂單編號當去重鍵,用原子操作標記:SET NX(key 不存在才寫入;「檢查有沒有」和「寫入」一步完成,中間不會被別人插隊)成功才執行副作用,失敗代表別人已經做過了。marker 要設明確的 TTL,不然 Redis 會被去重鍵塞滿。

順序也一樣。SQS 標準佇列不保證順序,所以「先建立後取消」可能以相反順序到達你的 worker。需要順序就用 FIFO,但要先知道它的代價。

FIFO 的代價

FIFO 買到順序和去重,付出的是吞吐上限。依 AWS 文件(2026-09-20 查證):

  • 一般模式(沒開高吞吐模式)下,送、收、刪每種動作每秒 300 筆
  • 一般模式下開啟批次(一次呼叫最多帶 10 則),每種動作每秒最多 3,000 則,也就是 300 次呼叫 × 10 則
  • 訊息大小上限 1 MiB,保留最長 14 天
  • 可見性逾時最長 12 小時(訊息被一個消費者拿走後,對其他人隱藏多久;時間到還沒刪掉,就會重新出現給別人拿)

300 筆不夠用,要開高吞吐模式(high throughput FIFO)。這裡要先認識 FIFO 的一個欄位:訊息群組 ID(MessageGroupId),送訊息時由你指定,同一個群組 ID 裡的訊息保證照順序處理。開了高吞吐模式之後,吞吐上限改成照訊息群組 ID 分散計算。SQS 把群組 ID 拿去 hash,決定訊息存進哪個分區(partition,SQS 內部存訊息的單位);每個分區各自有上限(官方文件)。所以全部訊息塞同一個群組 ID,開了也沒用,全擠在同一個分區。用訂單編號當群組 ID,不同訂單才會分散開、彼此並行。

(官方兩頁說法有出入:配額頁寫一般模式的 300 是「每個分區」,開啟高吞吐的設定頁寫一般模式的配額算在整條佇列。估容量時照比較保守的「整條佇列 300」算。)

自架 vs AWS

自己架 RabbitMQ 或 Kafka,你要管叢集、磁碟水位、分區重新平衡、消費者延遲監控。SQS 這些全不用管,也沒有「佇列機器掛了」這件事。

代價是你放棄了細節控制。RabbitMQ 的路由規則、Kafka 的壓縮主題(compacted topic,同一個 key 只保留最新一筆),SQS 都沒有對應物。真的需要那些,就是該自架或用 MSK 的時候。

K8s 映射

在 K8s 上這套的對應物通常是自架 RabbitMQ 或 Redis Streams,配 KEDA 依佇列長度自動擴 worker。

AWS 這邊對應的是 SQS 加 ECS Service Auto Scaling,指標用 ApproximateNumberOfMessagesVisible。

冪等和死信佇列那兩條在 K8s 一模一樣——換平台不會讓重複扣款消失,那是訊息模型帶來的,不是雲廠商帶來的。

相關文章


非同步不是效能優化,是把「做不完」跟「回不了」這兩件事拆開。前者慢慢解決,後者不能等。