來看一個每個電商後端都遇過的場景。
使用者按下「結帳」,這一下你後端要做的事可不只「建一筆訂單」:要寄確認信、要發紅利點數、要通知倉庫揀貨、要更新推薦模型、可能還要推一則 App 通知。如果你把這些全部塞在同一個 request 裡一件一件做完才回他「下單成功」——
那使用者就只能盯著轉圈圈,等你把信寄完。更慘的是,萬一寄信的服務剛好掛了,整筆訂單跟著失敗。欸不是,我只是想買個東西,干寄信什麼事?
問題的根,是你把「該馬上做的(建訂單)」跟「晚點做也沒差的(寄信、發點數)」綁死在一起了。Queue 就是來幫你把這兩種事拆開的。
它到底是什麼
Queue 就是一條排隊的隊伍:有人把任務丟進去(producer),有人從另一頭一個一個拿出來做(consumer),中間那條線就是 queue。
回到剛剛的下單。有了 queue,流程變成這樣:建好訂單,馬上回使用者「成功」;至於寄信、發點數、通知倉庫,通通寫一張紙條丟進 queue 就不管了。後面有專門的 worker 慢慢從 queue 撈出來做。使用者早就拿到「下單成功」走人了,根本不用陪你等寄信。
這帶來兩個你立刻有感的好處:
第一,快。 使用者只等「真正必要」的那步,副作用全部甩到背景。
第二,穩。 寄信服務掛了?沒關係,紙條還乖乖躺在 queue 裡,等它復活再撈出來重做就好——訂單一點事都沒有。這就是「解耦」講的東西:倉庫的事歸倉庫,別來拖累下單。
「那我開一個背景 thread 跑不就好了」
又是這個經典直覺對吧。寄信慢,那我開個 thread 在背景寄、主線程先回不就好了:
# 土法煉鋼版
def checkout(order):
create_order(order)
threading.Thread(target=send_email, args=(order,)).start() # 丟背景
return "下單成功"Demo 階段這樣是會動啦。但它一樣會在幾個地方翻車,而這些翻車點,正好就是「為什麼你需要一個真正的 Queue」:
Thread 掛了,事情就人間蒸發。 那個背景 thread 寄信寄到一半,程式重啟、或它自己拋例外死掉——這封信永遠不會被寄出,而且沒有人知道。沒有重試、沒有紀錄,就這樣沒了。Queue 不一樣,任務沒被成功 ack 之前,它一直躺在裡面,掛了還能重來。
Thread 跨不出這台機器。 你的 worker 想擴充成五台一起消化?thread 只活在自己 process 裡,幫不了你。Queue 是大家共用的,你要幾台 worker 來搶著做都行。
爆量的時候 thread 會直接淹死你。 秒殺一來,瞬間幾萬個請求,你就瞬間開幾萬個 thread?記憶體先爆給你看。Queue 則是讓任務乖乖排隊,worker 按自己消化得了的速度慢慢拿——這招有個名字,叫削峰填谷。
削峰填谷,這四個字是 Queue 的靈魂
想像你的 DB 每秒只吞得下 5000 筆,但促銷一開,尖峰瞬間湧進 5 萬筆。差 10 倍。
沒有 queue,這 5 萬筆一起撲上去,DB 當場躺平,連帶把本來好好的使用者也一起拖下水。
有 queue,這 5 萬筆先全部進去排隊(削峰——把那個嚇人的尖峰削平),worker 在後面穩穩地每秒撈 5000 筆出來做,花個幾秒到十幾秒慢慢消化完(填谷——把波峰的量攤平到後面的空檔)。
使用者體感可能就是「通知晚個幾秒到」,但你的系統從頭到尾沒倒。拿一點點即時性,換整個系統在尖峰不崩——這筆交易,九成場景都划算。
當然,天下沒有白吃的午餐
Queue 把同步變非同步,爽是爽,但你得吞下幾個新麻煩:
- 事情變成最終才會完成,不是「馬上保證做完」。使用者下單成功的當下,信其實還沒寄出去。這對「寄信」沒差,但對「要即時給結果」的事情(例如查餘額)就不能這樣搞。
- 同一張紙條可能被撈到兩次(網路抖一下、worker 做到一半掛了重來),所以你的 consumer 得做到「重複做也不會出錯」——這叫冪等(idempotency)。
- 任務失敗了要怎麼辦?重試幾次?一直失敗的丟去哪?(這就是之後會講的重試策略跟死信佇列。)
所以判斷「該不該上 Queue」的關鍵問題是:這件事,使用者需要當場就拿到結果嗎? 需要 → 別丟 queue,乖乖同步做完。不需要、晚一點點也 OK → 丟進去,讓系統喘口氣。
個人經驗:我自己在 Laravel 上踩過一個有點荒謬的版本。當時專案根本沒有一台專門的 queue 機,卻硬做了 event-driven——結果就變成 MVC 加 EDA 各半套的四不像,一出事 debug 超痛苦,因為你永遠不確定一個動作到底是同步走 controller、還是被某個 event handler 非同步接走了。
真正有好好用到 queue 的,反而只有那種「一個完整動作」的場景——寄某封信、跑某張報表,就 cronjob + queue 一包丟出去。本質上是有做到非同步沒錯,但整體就是一種「做一半」的感覺:該解耦的沒解乾淨,不該非同步的地方反而非同步了。這也是為什麼我會一直強調——用 queue 之前先想清楚哪些事該丟、哪些不該丟,半套的 event-driven 真的比老老實實的同步還難維護。
接下來往哪走
這篇只想讓你抓到 Queue 在解什麼問題。再往下就是工具跟模式的世界了:
- 為什麼不能只靠 async / thread,更完整的版本 → 本章
02-why-not-just-async(規劃中) - 那麼多 broker 到底選哪個 → RabbitMQ vs Kafka vs NATS vs Redis Streams:10 分鐘做出選型
- Queue 是怎麼一路演化成現在這樣的 → Message Queue 演進史、Queue 演進驅動力
- 想看實戰素材的 → Queue、Redis Cache、Cold Start:最後三場測試、Event-driven & Queue:非同步處理與訊息佇列
反思
Queue 不是什麼高大上的東西,它就是在回答一個很樸素的問題:「進來的速度比我做得完的速度還快,怎麼辦?」
答案是別硬扛——讓它排隊,你按自己的節奏慢慢消化。
下次要決定一件事該不該丟 queue,問自己三題就好:
- 這件事,使用者需要當場拿到結果嗎?(要 → 同步做;不要 → 可以丟)
- 它晚個幾秒、甚至失敗重做幾次,會出事嗎?(不會 → 很適合 queue)
- 我的 consumer 被重複叫到會不會壞掉?(會 → 先把冪等補上再說)
三題想清楚,你大概就知道這東西到底要不要進 queue 了。