🎯 什麼情境該想到我

當你「訊息亂序到達,需要依序號重排回正確順序再往下送」的時候。

⚙️ 怎麼用(步驟 / 公式)

意圖:把一串亂序到達的相關訊息,依序號重新排成正確順序後輸出。

做法要點(重排序器是有狀態元件):

  • 依每則訊息的序號(sequence number)暫存收到的訊息。
  • 一旦湊到連續的順序,就依序放出。
  • 只調整順序、不改動訊息內容。

★★ 先問:真的需要排序,還是只需要「最終狀態正確」?

// ★★ 多數情況的正確答案:不要排序,用版本號讓亂序無害
UPDATE read_model SET status = $1, version = $2
WHERE id = $3 AND version < $2          // ★ 舊訊息影響 0 列,直接丟棄

★★ 這一招勝過重排序器的三個理由:
① 無狀態——不必存半成品、不必逾時、不必恢復。
② 可完全並行——不必等前一則。
③ 順便解掉重複——重複訊息的 version 不大於已存的,自然被忽略。

★ 重排序器只在「處理本身有順序副作用」時才必要:
例如要按順序寫進一個 append-only 的檔案、或要按順序呼叫一個不冪等的外部 API。
如果你只是在更新狀態,就用版本號。

★ 真的需要時:實作要點

type ResequencerState struct {
    SequenceID   string
    NextExpected int                        // ★ 下一個要輸出的 position
    Buffered     map[int]json.RawMessage    // ★ 已收到但還不能輸出的
    FirstSeenAt  time.Time                  // ★ 逾時基準
}
 
func accept(st *ResequencerState, pos int, payload json.RawMessage) []json.RawMessage {
    st.Buffered[pos] = payload
    var out []json.RawMessage
    for {
        p, ok := st.Buffered[st.NextExpected]
        if !ok { break }                    // ★ 缺號了,停在這裡等
        out = append(out, p)
        delete(st.Buffered, st.NextExpected)
        st.NextExpected++
    }
    return out                              // ★ 可能一次輸出一串(缺的那則到了)
}

★ 注意「一次輸出一串」這個行為:收到缺失的第 3 則時,
會一口氣輸出 3、4、5、6……——下游要能承受這種突發。

★★ 三個會讓它在生產環境爆掉的問題

① ★★ 缺號永遠不來 → 緩衝無限成長 → OOM
   第 3 則進了 DLQ / 被判無效 / 生產者崩潰前沒發出
   → ★ 必須有:逾時 + 緩衝上限 + 「跳號」策略
   → ★★ 跳號時一定要記錄「跳過了哪幾號」,否則是靜默資料遺失

② ★★ 有狀態 ⇒ 無法水平擴展(同一個 SequenceID 必須由同一個實例處理)
   → ★ 只能用「分區鍵 = SequenceID」把同一組導到同一個實例
   → ★ 而這代表:一個熱門 SequenceID 就是一個熱點,無法分散

③ ★ 狀態必須持久化
   重啟後緩衝清空 → ★ 已經收到但還沒輸出的訊息全部遺失
   (它們已經被 ack 了,broker 不會再送)
   → ★★ 所以:要嘛狀態持久化,要嘛「輸出後才 ack」(但那會讓緩衝的訊息一直
      未確認,撞上 ack 期限)

★★ 第 ③ 點的兩難是重排序器最根本的困難:
「先 ack 再緩衝」→ 重啟丟資料;「緩衝完才 ack」→ 撞上 visibility timeout/ack deadline。
唯一乾淨的解是狀態持久化——而那就是一個要維運的有狀態服務。

★ Broker 層面能不能幫你?

broker能不能
Kafka★ 單一分區內本來就有序 → 用 SequenceID 當 key,完全不需要重排序器
RabbitMQ單 queue + 單 consumer 才有序 → 犧牲並行度換順序
SQS FIFO★ 同一個 MessageGroupId 內有序 → 用 SequenceID 當 group id
SQS 標準★★ 完全無序 → 這才是真正需要重排序器的場景

★★ 結論:在 Kafka 和 SQS FIFO 上,你幾乎永遠不需要這個模式——
把 SequenceID 當 partition key/message group id 就解決了,
代價是「同一組只能單執行緒處理」,而那正是順序的本質代價。

★ 兄弟辨析

差別
聚合器★ 同樣有狀態,但合併成一則;重排序器輸出仍是 N 則
訊息序列提供 position/total 的那一端
分割器產生序列的那一端
點對點通道順序 vs 並行的根本取捨

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 需緩存並追蹤序號,可能因缺號而卡住,須設緩衝上限或逾時。

  • 與聚合器都有狀態,但重排序器輸出多則(只是排序),不合併成一則。

  • 訊息本來就有序時不需要。

  • ★★ 先試「用版本號讓亂序無害」(WHERE version < $new)。無狀態、可並行、順便解重複。重排序器只在「處理有順序副作用」時必要。

  • ★★ 在 Kafka 與 SQS FIFO 上幾乎不需要這個模式:用 SequenceID 當 partition key/message group id 就有序了。

  • ★★ 「先 ack 再緩衝」重啟會丟資料;「緩衝完才 ack」會撞 ack 期限。 唯一乾淨解是狀態持久化。

  • ★★ 缺號可能永遠不來(進 DLQ/被判無效)。必須有逾時 + 緩衝上限 + 跳號策略,而跳號要記錄跳了哪幾號。

  • ★ 有狀態 ⇒ 同一個 SequenceID 必須由同一實例處理 ⇒ 熱門 SequenceID 就是熱點。

  • ★ 缺失的那則到了會一次輸出一串,下游要能承受突發。

  • 順序的本質代價是「同一組只能單執行緒處理」。無論用哪種手段都逃不掉。

🔗 相關工具