Skip to main content

Command Palette

Search for a command to run...

Lesson 23: 系統的緩衝區-Queue 佇列與非同步處理 (Asynchronous)

Published
2 min read

佇列

佇列的實作工具非常多,舉凡AWS SQS、RabbitMQ、Kafka…等。

佇列的特性,其實是一個非常強大的系統緩衝區,應用層面非常廣。

什麼是佇列?

佇列可以想像成,在既有流程中外,有另一個”水管”,來連接原有的資料流(或邏輯過程),其中 呼叫方將資料 推(Push)到水管中,接受方(監聽) 從水管中將資料拉(Pull)出處理

為什麼佇列是「強大的緩衝區」?

在同步處理中,系統像是一條緊繃的繩子,任何一個節點斷了或慢了,整個流程就會崩潰。而加入佇列後,系統擁有了以下三個關鍵特性:

削峰填谷(Load Leveling)

當瞬間流量(例如雙 11 搶購)暴增時,後端資料庫通常無法承受。佇列充當了緩衝墊,讓請求先「排隊」,後端再依據自身的處理能力慢慢消耗,避免伺服器因過載而宕機。

非同步解耦 (Asynchronous Decoupling)

呼叫方不需要等待處理結果。以「註冊帳號」為例:

  • 同步: 寫入資料庫 → 寄送驗證信 → 傳送歡迎簡訊 → 回傳成功(若簡訊服務掛了,註冊就失敗)。

  • 非同步: 寫入資料庫 → 丟入 Queue → 回傳成功。寄信與簡訊由後端 Consumer 慢慢處理。

容錯與重試機制 (Fault Tolerance)

如果接收方 在處理時暫時故障,資料依然停留在佇列中。待接收方修復後,可以從上次中斷的地方繼續處理,確保資料不丟失。

核心算法

如果講到佇列,不稍微聊一下FIFO,內容就有點空洞了,FIFO是佇列的核心特性,與堆疊(Stack)的 LIFO形成對比。

FIFO

FIFO是First In , First Out。白話文就是先進先出,先進到佇列的資料也會優先被調出佇列。

  • 公平性: 確保請求按照收到的先後順序處理,不會有後來的請求「插隊」導致先來的請求無窮盡等待(Starvation)。

  • 順序一致性: 在某些情境(如銀行轉帳、訂單狀態變更),處理順序絕對不能錯亂。例如「扣款」必須發生在「出貨」之前。

佇列的變體與進階演算法

在實務的後端開發中,單純的 FIFO 有時不足以應付複雜需求,因此衍生出以下幾種常見模式:

1. 優先權佇列 (Priority Queue)

並非所有資料都一視同仁。例如:

  • 高等級: 處理使用者的付款請求(需立即處理)。

  • 低等級: 處理每週報表的發送(可以稍後再做)。 這時演算法會根據資料的 Priority Tag 重新排序,讓高等級的資料「合法插隊」。

2. 環形佇列 (Circular Queue)

在記憶體有限的情境下(例如嵌入式系統或高效能 Buffer),當指標到達陣列末端時,會繞回開頭重新利用空間。這避免了頻繁搬移資料的效能損耗。

3. 延遲佇列 (Delay Queue)

資料推入後,並不會立刻被 Consumer 拉走,而是設定一個「冷卻時間」。

  • 應用場景: 訂單成立後 30 分鐘若未付款,自動取消訂單。

補充說明:分散式系統中如何保證順序性

在分散式系統中,「順序性」是一個極大的挑戰。因為分散式架構的核心是「並行」,而順序要求的則是「串行」。這兩者本質上是衝突的。

要保證順序,通常需要從 生產端、存儲端、消費端 三個環節同時下手:

1. 生產端:分組標記

在海量資料下,我們不可能要求「全球順序」,那會造成嚴重的效能瓶頸。實務上,我們保證的是「局部順序」。

  • 機制: 給予每條訊息一個 MessageKey(例如 order_iduser_id)。

  • 演算法: 使用一致性雜湊(Consistent Hashing)確保擁有相同 Key 的訊息,永遠會進入同一個 PartitionShard

  • 結果: 雖然訂單 A 和訂單 B 之間沒有順序關係,但「訂單 A 的建立」與「訂單 A 的付款」會排在同一條水管裡。

2. 存儲端:強順序佇列(FIFO Queue)

一般的雲端佇列(如 AWS SQS Standard)為了效能,通常只保證「盡力交付(Best-effort ordering)」。若要嚴格順序,必須選用 FIFO 類型 的工具:

  • AWS SQS FIFO: 透過 Message Group ID 確保同組訊息嚴格先進先出,並提供 Deduplication ID 防止重覆發送。

  • Kafka: 內建保證單一 Partition 內的訊息是絕對有序的。

3. 消費端:單一消費者模式

這是最容易出錯的地方。即便佇列是有序的,如果後端開了 10 個 Worker同時拉資料,順序就會亂掉:

  • 訊息 1:進入 Worker A(處理慢,需 5 秒)。

  • 訊息 2:進入 Worker B(處理快,需 1 秒)。

  • 結果: 訊息 2 會比訊息 1 先完成,破壞了邏輯順序。

解決方案:

  • 分區消費: 確保一個 Partition 同一時間只能被一個 Consumer 讀取(Kafka 的預設行為)。

  • 樂觀鎖(Optimistic Locking): 在資料庫增加 version 欄位。即便訊息 2 先到,它發現版本號不對,會拒絕寫入並要求重試。

4. 終極殺手鐧:時序標記

如果系統非常複雜,無法依賴中間件的順序,我們會在訊息內核嵌入「邏輯時鐘」:

  • 雪花演算法 (Snowflake ID): 生成帶有時間戳且遞增的唯一 ID。

  • 狀態檢查: 消費端維護一個 last_sequence_id。如果收到的 ID 是 5,但上次處理的是 3,表示訊息 4 掉包或延遲了,此時 Consumer 應將 5 暫存或報錯,直到 4 出現。

總結:代價與權衡

保證順序性是有代價的:

  1. 效能下降: 無法充分利用多核並行處理,吞吐量會受限於單一 Partition 的處理能力。

  2. 可用性風險: 如果某條訊息處理卡住(Poison Pill),後面所有的有序訊息都會被堵死。

理解「什麼時候不需要保證順序」跟「如何保證順序」一樣重要。盡可能讓業務邏輯具備 冪等性(Idempotency),才是減輕順序依賴的最佳解。

非同步處理

探討 非同步 時,最核心的觀念轉變是:「從 等待結果 變成 訂閱通知」。

同步 vs. 非同步

同步處理 (Synchronous) — 點餐

走道7-11點了一杯大冰拿,點完後就站在櫃檯前死守,直到咖啡拿在手上,才離開去滑手機。

  • 問題: 如果咖啡機壞了(I/O 阻塞),後面的排隊人群(其他 Request)全都會被卡死。

非同步處理 (Asynchronous) — 呼叫器點餐

點完咖啡後,店員給你一個呼叫器。可以先回位子坐下處理公事、回 Email(繼續執行其他任務)。當呼叫器響了(Event Triggered),你再去取餐。

  • 優點: 櫃檯(CPU/Thread)可以立刻處理下一個人的點餐,系統吞吐量極大化。

非同步處理的關鍵元件

要實作非同步,系統通常需要以下三個部分協作:

呼叫方 (Caller)

發起請求後不再等待,而是立即返回執行下一行程式碼。

回呼機制 (Callback / Promise / Future)

當任務完成時,系統如何通知你?

  • Callback: 「做完後請執行這個 function」。

  • Promise / Async-Await: 「這是一個承諾,未來我會把結果填進去」。

事件迴圈 (Event Loop)

這是非同步的心臟(常見於 Node.js 或 Go)。它不斷檢查「任務完成了嗎?」如果完成了,就把對應的回呼函式丟回主執行緒執行。

非同步 與 佇列 的結合

非同步處理通常依賴 Queue 來實現系統間的解耦:

  1. 提交任務: API 收到請求,把繁重的任務(如:生成 PDF、發送萬封郵件)丟進 Queue。

  2. 立刻回應: API 告訴使用者「已收到請求,處理中」,耗時僅需 10ms。

  3. 非同步消耗: 後端有一個 Worker 默默地從 Queue 拿任務出來跑。

  4. 通知結果: 跑完後透過 WebSocket 或 Webhook 通知前端。

進階思考:非同步的代價

雖然非同步很強大,但身為 工程師,我們必須考慮以下挑戰:

  • Race Condition: 當兩個非同步任務同時修改同一個資料庫欄位,誰才是對的?

  • Error Handling: 非同步錯誤很難追蹤(Stack Trace 通常會斷掉)。如果背景任務失敗了,使用者怎麼知道?

  • 複雜度: 程式碼不再是從上到下執行,除錯與邏輯串接變得困難。

從Rookie到Junior,一個後端成長的30堂課

Part 4 of 31

“從 Rookie 到 Junior:一個後端成長的 30 堂課” 是一套專為後端新手所設計的成長型技術系列文章。內容以實務為導向,逐步拆解後端工程的核心能力,包括程式語言基礎、架構思維、框架運作原理、業務邏輯設計、資料庫操作、以及常見的開發模式。 本系列的目標是協助讀者從零散的學習堆疊,建立成體系的後端知識框架。讀者能夠理解各語言背後不變的工程思維與設計原則。這套內容旨在讓學習者從「會寫程式」進入「能理解系統設計」的階段,逐步具備勝任 Junior Backend Engineer 的能力。

Up next

Lesson 22: 快取災難預防:快取穿透 、擊穿與雪崩

我們在上一篇文章中介紹了基本的Cache Aside Pattern,也補充了在Database 主從分離架構下可能造成Cache的異常,並一同介紹了:延遲雙刪 以及 CDC。 我們這個章節要來談談Cache還有哪些問題 快取穿透 (Cache Penetration) 定義 請求的資料 不在快取中,也不在資料庫中。 每次請求都會穿過快取,直接打到 DB,但 DB 也查不到資料,導致無法回寫快取。如果有惡意攻擊者使用大量不存在的 ID 進行攻擊,DB 會瞬間承受巨大壓力。 常見場景 惡意攻擊:...

More from this blog

Lesson 26 : 系統韌性的守護者-限流、熔斷與背壓的設計模式

當這幾個名詞出現後,代表我們進到了一個高併發/大流量的系統了。在這個章節中,我們一起來看看如何透過一些方式來避免高併發導致我們的系統crash掉。 限流(Rate Limiting) 相信大家對這個名詞並不陌生,限流其實就是字面上的含意,限制流量。 限流的目的是保護「接收方」,確保系統不會因為瞬間的高併發請求而癱瘓。 限流通常發生在 API Gateway 或服務的最前端。它像是一個夜店門口的保全

Mar 26, 20262 min read

Lesson 25: 淺談 單體架構、微服務架構與單/多租戶架構

過去我們討論了 要把程式寫在哪、程式要怎麼拆的題目,接下來我們來看看「如何服務不同客戶」,這些架構反映了軟體開發在擴充性與複雜度之間的權衡。 軟體架構深度解析:從系統拆分到商業規模化 在軟體工程的演進中,架構的選擇往往是在「開發效率」、「系統擴充性」與「營運成本」之間尋求平衡。我們可以從兩個核心維度來觀察這些架構:系統如何運行(單體 vs. 分散式) 以及 如何服務客戶(單租戶 vs. 多租戶)。

Mar 26, 20262 min read

Lesson 24: 資料庫擴展術-讀寫分離、複寫機制與快取一致性挑戰

為什麼要讀寫分離? 大多數的 Web 應用都是 「讀多寫少」(例如:看文的人多,發文的人少,Heavy Read System)。當所有的請求都塞給同一台資料庫時,磁碟 I/O 和連線數會成為瓶頸。 Master (主庫): 負責寫入 (Insert/Update/Delete),確保數據一致性。 Slave (從庫): 負責讀取 (Select),可以有多個從庫來分擔讀取壓力。 為什麼讀

Mar 25, 20262 min read

面試經驗談 2025-2026

從2025年3月開始,我陸陸續續參與了從新創到上市櫃公司的Senior - Tech Lead的相關面試,其中有不乏 尊重面試者、展現高度專業的企業(公司),當然也有遇到幾場面試鬼故事,這篇文章主要分享我對於軟體工程師面試的方向分享,以及部分鬼故事,以此警惕自己不要成為這樣的面試官。 AI的洪流,改變了SWE的生態 LLM的發展確確實實的影響到了軟體工程師的生態。 過去受限於算力與資料規模,深度學

Mar 23, 20262 min read

Bennett's Tech Blog | 後端架構、系統設計

32 posts

來自台灣的軟體工程師,相信軟體可以改變世界