🎯 什麼情境該想到我

當你「一份資料太大、或是連續的一大串,得拆成多則有序訊息來傳」的時候。

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

意圖:用一組帶有序資訊的訊息,傳遞任意大量或連續的資料。

做法要點:每則訊息標頭帶三項欄位:

  • 序列識別碼:同一序列的訊息共用,用來歸屬。
  • 位置編號:這是序列中的第幾則。
  • 序列結束標記:標示是否為最後一則。
    接收端據此判斷是否收齊、並依序重組成完整資料。

★★ 先考慮不要用它:讓處理與順序無關,比重建順序便宜得多

// ★★ 最實用的做法:訊息帶版本號,消費端丟棄過舊的
type OrderUpdated struct {
    OrderID string `json:"order_id"`
    Version int64  `json:"version"`      // ★ 來源的單調遞增版本
    Status  string `json:"status"`
}
 
// 消費端:條件更新(樂觀鎖)
res, err := tx.Exec(ctx, `
    UPDATE order_read_model SET status = $1, version = $2
    WHERE order_id = $3 AND version < $2`,    // ★★ 舊訊息不會蓋掉新狀態
    e.Status, e.Version, e.OrderID)
// ★ 影響 0 列 = 這則訊息過舊,直接 ack 掉

★★ 這一招同時解掉三個問題:亂序、重複、以及「可以完全並行處理」。
而 訊息序列/重排序器 只解掉亂序,代價是有狀態元件與逾時管理。
★ 所以順序的第一個問題永遠是:真的需要「按順序處理」,還是只需要「最終狀態正確」?
多數情況是後者(見 工具-交易與隔離等級 的樂觀鎖)。

★ 真的需要序列時:三個必備欄位

type SequencedMessage struct {
    SequenceID string `json:"sequence_id"`  // ★ 這一組的識別(= correlation id)
    Position   int    `json:"position"`     // ★ 第幾則,從 1 開始
    Total      int    `json:"total"`        // ★★ 共幾則;-1 表示「未知,等 IsLast」
    IsLast     bool   `json:"is_last"`      // ★ 串流式(總數事先未知)時必備
}

★ Total 與 IsLast 要至少有一個,否則接收端不知道什麼時候收齊了
(這正是 聚合器 的「完成條件」問題)。
⚠️ Total 事先未知的情況很常見(串流、分頁、邊產生邊送)→ 那就必須有 IsLast。

★ Broker 的原生順序保證(各家差很多)

broker順序保證範圍
Kafka★ 單一分區內嚴格有序。跨分區完全無保證 → 用同一個 key 進同一分區
RabbitMQ★ 單一 queue + 單一 consumer 才有序。多 consumer 就沒了
SQS 標準★★ 完全不保證順序
SQS FIFO★ 同一個 MessageGroupId 內有序(有吞吐上限)
NATS JetStreamstream 內有序

★ max.in.flight.requests.per.connection 的陷阱(Kafka):
預設是 5。未開啟冪等時,重試會讓後面的批次先成功 ⇒ 順序亂掉。
✅ 但 enable.idempotence 預設為 true(Kafka 4.0,2026-09-25 查證),
在 ≤5 的情況下 broker 會依 sequence number 重排 ⇒ 順序仍有保證。

⚠️ 手動把 enable.idempotence 關掉 + in-flight > 1 = 靜默的亂序。

★★ 序列最難的部分:缺漏怎麼辦

★ 收到 1、2、4 —— 3 在哪?
  ① 網路慢,稍後會到          → 等(★ 但要等多久?)
  ② 發生者根本沒發            → 永遠等不到
  ③ 進了 DLQ                 → ★ 永遠等不到,而且你不知道
  ④ 被判定無效丟棄            → 同上

必須做的三件事:

① ★ 逾時:等不到就放棄,並且「明確記錄缺了哪幾則」(不要靜默不完成)
② ★ 記憶體上限:等待中的部分序列要有數量與大小上限,否則一個壞的生產者能 OOM 你
③ ★★ 持久化:等待狀態不能只在記憶體。重啟後必須能繼續等(或明確放棄)

★ 這三件事讓「有狀態的序列處理」變成一個要維運的元件——
這正是為什麼「讓處理與順序無關」那條路更值得先試。

★ 兄弟辨析

差別
分割器★ 產生序列的那一端(一則拆成 N 則,加上 position/total)
聚合器★ 把序列合回一則(需要完成條件)
重排序器★ 只排序不合併——輸出仍是 N 則,但按順序
關聯識別碼SequenceID 通常就是它
點對點通道順序 vs 並行的根本取捨在那裡

🧪 我實際套用的紀錄

  • (待填)

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

  • 接收端需能緩存並重組,還要處理缺漏。

  • 若訊息可能亂序到達,搭配重排序器先排好。

  • 與分割器產生多則訊息的概念相關,但這裡強調「同一份資料的有序切片」。

  • ★★ 先考慮「讓處理與順序無關」:帶版本號 + 條件更新(WHERE version < $new)同時解掉亂序、重複、並行三個問題。

  • ★ Total 與 IsLast 至少要有一個,否則接收端不知道何時收齊。串流情境必須有 IsLast。

  • ★★ Kafka 的順序保證只在單一分區內。SQS 標準佇列完全不保證順序。

  • ★ 手動關掉 enable.idempotence 且 in-flight > 1 = 靜默亂序(預設是開啟的,2026-09-25 查證)。

  • ★★ 缺漏的那一則可能永遠不來(進了 DLQ 或被判無效)。必須有逾時,而且要明確記錄缺了哪幾則。

  • ★ 等待中的部分序列要有數量與大小上限,否則一個壞生產者能讓你 OOM。

  • ★ 等待狀態必須持久化,否則重啟後所有半成品序列都變成永遠等不到。

  • 有狀態的序列處理是要維運的元件(逾時、記憶體、狀態恢復)——這是它真正的成本。

🔗 相關工具