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

# 佇列

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

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

## 什麼是佇列？

![](https://cdn.hashnode.com/uploads/covers/693a6e8cf7ff0b7590781231/747e619f-e921-4531-aa6d-5a992c072c0e.png align="center")

佇列可以想像成，在既有流程中外，有另一個”水管”，來連接原有的資料流(或邏輯過程)，其中 呼叫方將資料 推(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_id` 或 `user_id`）。
    
*   **演算法：** 使用一致性雜湊（Consistent Hashing）確保擁有相同 Key 的訊息，永遠會進入同一個 **Partition** 或 **Shard**。
    
*   **結果：** 雖然訂單 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 通常會斷掉）。如果背景任務失敗了，使用者怎麼知道？**
    
*   **複雜度： 程式碼不再是從上到下執行，除錯與邏輯串接變得困難。**
