你按下「送出」後,伺服器真的要全部做完嗎?
假設一個網站提供「上傳影片後寄 Email 通知」的功能。使用者按下送出後,後端可能要做很多事:驗證檔案、寫入資料庫、轉換影片格式、產生縮圖、上傳儲存空間、寄出通知信。
最直接的寫法,是讓同一個 request 從第一步一路執行到最後一步。這叫做同步處理:前面的工作沒完成,後面的工作就不能開始,而使用者也只能繼續等待。
如果整套流程只花 300 毫秒,通常沒有問題。但影片轉檔可能需要幾十秒,第三方 Email API 可能突然變慢,甚至暫時故障。此時使用者明明只是想確認「檔案有沒有成功送出」,卻被迫等待所有後續工作完成。
這就是 Message Queue 常出現的地方。
Message Queue 的核心想法其實很簡單
Message Queue 可以先理解成一張「待辦工作清單」。
原本的流程是:
加入 Queue 後,可以變成:
Consumer
在背景完成工作
Web Server 不再需要親自完成所有事情。它只需要確認輸入有效、保存必要資料,接著建立一則訊息,例如:
{ "type": "generate_thumbnail", "videoId": "v_123" }
這則訊息不是工作結果,而是「請處理這件事」的描述。
之後另一個程式看到訊息,再真正執行縮圖產生工作。於是接收 HTTP request 和執行耗時任務,被拆成兩段獨立流程。
Producer、Queue、Consumer
理解訊息佇列時,最常看到三個角色。
Producer 是產生訊息的一方。例如 API Server 收到影片上傳後,建立一個轉檔任務。
Queue 是暫時保存訊息的地方。當工作一時做不完,訊息可以先排在這裡等待。
Consumer 則負責取得訊息並執行工作。Consumer 也常被稱為 Worker。
因此一條最基本的資料流就是:
重要的是,Producer 通常不需要知道究竟是哪一台 Consumer 處理工作。Consumer 也不必和 Producer 同時在線。兩邊透過 Queue 解耦,系統因此得到更大的調度空間。
為什麼不直接呼叫另一個 API?
假設 Web Server 可以直接呼叫轉檔服務:
看起來已經把服務拆開了,但兩者仍然在時間上互相依賴。如果 Transcoding API 當下沒有回應,Web Server 就必須等待、重試,或者直接失敗。
Queue 多了一層緩衝。
Web Server 只要成功把任務交給 Queue,就可以結束自己的主要工作。轉檔服務暫時停機時,尚未處理的訊息仍可留在 Queue;服務恢復後再繼續消化。
這種特性常被稱為 temporal decoupling:兩個元件不必在同一時間都正常運作,才能交換工作。
Queue 也是流量的緩衝區
想像平常每分鐘只有 100 個圖片處理任務,Worker 每分鐘能處理 150 個,一切正常。
某個活動開始後,突然在一分鐘內湧入 5,000 個任務。如果沒有 Queue,後端可能同時啟動大量昂貴工作,CPU、記憶體、資料庫連線與第三方 API 都可能瞬間被打滿。
有 Queue 時,5,000 個任務可以先排隊。Worker 仍按照系統能承受的速度處理。
因此 Queue 不會讓工作憑空變快。它真正做到的是把「流量進來的速度」和「系統處理的速度」分開。
這種吸收突發流量的能力,常被稱為 buffering 或 load leveling。
一個 Consumer 不夠,就增加 Consumer
如果一個 Worker 每秒只能處理 10 個任務,而 Queue 已經累積數萬筆訊息,可以在工作彼此獨立的前提下增加 Worker:
多個 Consumer 可以平行取得不同訊息,增加整體 throughput。
這也是 Queue 很適合背景工作系統的原因:前端 API 和背景運算可以分別擴充。網站 request 增加時擴充 Web Server;背景任務塞車時擴充 Worker,不需要把整個系統一起放大。
但是,「取到訊息」不等於「完成工作」
假設 Worker 從 Queue 取得一則「寄出通知信」的訊息,剛開始處理就突然當機。
如果 Queue 在 Worker 取走訊息的瞬間就永久刪除它,這封信就再也不會被寄出。
因此許多 Queue 系統會使用 acknowledgement,簡稱 ack。
典型流程是:Consumer 取得訊息、執行工作、成功後回報 ack,Queue 才把這則訊息視為已完成。
如果 Consumer 在 ack 前消失,系統可以讓訊息重新出現,交給同一個或另一個 Consumer 再處理一次。
這提高了可靠性,但也帶來另一個重要問題:同一件工作可能被執行不只一次。
At-most-once、At-least-once 與 Exactly-once
訊息系統常討論 delivery semantics,也就是「一則訊息到底可能被送幾次」。
At-most-once 表示訊息最多處理一次。好處是不會重複,但失敗時可能直接遺失。
At-least-once 表示系統努力確保訊息至少被處理一次。失敗時可以重新投遞,因此比較不容易漏掉工作;代價是同一則訊息可能被 Consumer 看見兩次以上。
Exactly-once 聽起來最理想:每件事情恰好發生一次。但在分散式系統中,「訊息只送一次」和「業務效果只發生一次」是不同問題。網路中斷時,Producer 或 Consumer 可能根本不知道上一個操作究竟成功了沒有。真正的 exactly-once 往往需要額外協議、儲存層支援與明確限制,不能只把它當成一個免費開關。
因此實務上很常見的設計是接受 at-least-once delivery,再讓 Consumer 具備 Idempotency。
Idempotency:同一件事做兩次,結果仍然正確
假設付款完成後有一個「增加會員點數 100 點」的背景任務。
如果訊息因為重試被執行兩次,而程式每次都直接執行:
points = points + 100
使用者就會得到 200 點。
這時可以替事件建立唯一 ID,例如 payment_abc123,並記錄這個事件是否已處理。Consumer 再次收到相同事件時,先檢查處理紀錄,發現已完成便不重複增加點數。
這就是冪等性設計的一種形式。
不是所有操作天生都 idempotent。把資料設定為某個固定狀態通常比較容易做到;「再增加一次」、「再寄一次」、「再扣一次」則必須特別注意重複執行造成的副作用。
Retry 不是無限重試
背景任務失敗時,重試很合理。例如第三方 API 暫時回傳 503,幾秒後可能就恢復。
但如果一則訊息內容本身就是錯的,例如缺少必要欄位,重試一萬次也不會成功。這種訊息有時被稱為 poison message。
因此成熟的 Queue 流程通常會限制 retry 次數,並搭配 exponential backoff:第一次失敗後稍等一下,第二次等待更久,之後逐步拉長間隔,避免故障期間持續轟炸下游服務。
超過重試上限後,訊息可以被移到 Dead-letter Queue(DLQ)。
Dead-letter Queue 不是垃圾桶
DLQ 用來保存「正常流程已經無法成功處理」的訊息。
工程師可以觀察 DLQ 數量、分析失敗原因、修正程式或資料,再決定是否重新投遞。若只是把失敗訊息丟進 DLQ 後永遠不看,問題只是從主 Queue 搬到另一個地方。
因此實務上通常需要監控 Queue depth、oldest message age、retry 次數與 DLQ 數量。Queue 是可靠性元件,也同時增加了一套必須被觀察的系統狀態。
訊息順序也沒有想像中簡單
多個 Consumer 平行工作時,A 可能需要 10 秒,B 只需要 100 毫秒,於是 B 反而先完成。失敗重試也可能改變處理順序。
如果業務真的要求嚴格順序,例如同一個帳戶的狀態事件必須依序套用,就必須在 Queue 的 partition key、consumer concurrency 或應用層設計中明確處理。要求「所有訊息全域嚴格排序」通常會犧牲平行處理能力。
Queue 和 Pub/Sub 不完全一樣
兩者都在傳遞訊息,但目的不一定相同。
工作佇列常強調「這件工作需要被某個 Worker 完成」。如果有五個 Worker,它們通常競爭任務,每則工作由其中一個處理。
Publish/Subscribe 則常強調「某件事發生了,所有有興趣的訂閱者都可以知道」。例如 order.created 事件可能同時被 Email、Analytics、Inventory 三個服務接收。
不同產品可能同時提供 Queue、Topic、Stream 或 Pub/Sub 能力,所以不能只看產品名稱判斷語意;真正要看的是訊息如何保存、投遞,以及多個 Consumer 之間如何分配訊息。
什麼工作適合放進 Queue?
常見例子包括 Email 寄送、圖片或影片處理、PDF 與報表產生、搜尋索引更新、Webhook 投遞、資料同步、批次匯入,以及耗時的 AI inference 工作。
它們通常有共同特徵:不需要在使用者眼前立即完成、可能耗時、容易受到外部服務影響,或者需要吸收突發流量。
例如使用者要求產生一份大型報表時,API 可以先建立 job,回傳 job ID 與「processing」狀態。Worker 在背景完成後,把狀態更新為 completed,前端再取得下載結果。
這比讓瀏覽器維持一條 HTTP connection 等五分鐘通常更容易管理。
但不要為了「架構看起來很專業」就加 Queue
Queue 不是免費的抽象層。
加入它之後,你需要處理訊息格式、序列化、retry、duplicate、timeout、監控、DLQ、Consumer 部署與訊息相容性。原本一個 function call 能理解的流程,會變成跨程序、跨時間的非同步流程,除錯也更困難。
如果工作只花幾毫秒、失敗必須立刻回報給使用者,而且系統流量很小,直接同步執行通常更簡單。
好的架構不是元件越多越好,而是在可靠性、延遲、吞吐量與複雜度之間做適當交換。
從一個 request 看完整流程
最後把整個概念串起來。
使用者上傳一張大型圖片。API 驗證請求並建立資料紀錄,接著把 image.process 任務送進 Queue,然後立即回傳 202 Accepted 與 job ID。
Worker 從 Queue 取得訊息,開始產生縮圖與壓縮版本。成功後更新資料庫狀態,再向 Queue ack。若處理失敗,訊息依 retry policy 延後重試;連續失敗超過限制後進入 DLQ,等待調查。
當流量突然增加時,Queue 暫時累積工作,而不是讓所有圖片處理程序同時壓垮 API Server。若積壓持續上升,系統可以增加 Worker 數量提高消化速度。
這就是 Message Queue 最重要的價值:它不是單純把工作「丟到背景」,而是在系統不同部分之間建立一個可以緩衝、重試、擴充與隔離故障的邊界。
結語
Message Queue 解決的核心問題,不是「如何讓程式更快」,而是「哪些事情真的需要現在一起完成」。
當一個 request 裡混進太多耗時、容易失敗或可以延後的工作時,Queue 可以把它們拆開:Producer 負責描述工作,Queue 保存待處理訊息,Consumer 在自己的節奏中執行。
但非同步系統也會帶來重複投遞、順序、重試與可觀測性等新問題。因此真正重要的不是知道 RabbitMQ、Kafka、SQS 或其他產品名稱,而是先理解 delivery semantics、idempotency 與 failure handling。
只要能回答「如果 Consumer 在最糟的時間點當機,這件工作最後會怎樣?」你就已經開始用正確的方式思考 Message Queue 了。
把概念放回真正的系統流程裡看,通常比只背名詞更容易理解它。