2. 增量消息乱序到达的缓冲重排与缺口检测

乱序缓冲的核心问题

交易所增量消息通常带有单调递增的序列号(如 seq),但网络多路复用、重连补发或推送线程调度会导致消息乱序到达。若直接按到达顺序更新订单簿,会出现“先应用 seq=105 再应用 seq=103”的错误,使价格档位数量、订单引用关系被破坏。本节实现一个乱序缓冲队列:以期望序列号 nextSeq 为基准,把超前到达的消息暂存,按序释放;同时检测缺口并记录,待缺口被填补后自动恢复更新。

缓冲重排的状态机与缺口检测

缓冲器只需维护三个量:nextSeq(下一条应应用的序列号)、buffer(按 seq 索引的暂存表)、gaps(已检测到的缺口区间)。每条消息到达时按以下规则处理:

  • seq == nextSeq:直接应用,然后连续从 buffer 中取出 nextSeq+1, nextSeq+2… 一并应用,直到出现空洞。
  • seq > nextSeq:说明中间有缺口,记录缺口 [nextSeq, seq-1],把消息放入 buffer。
  • seq < nextSeq:属于重复或过期消息,丢弃并计数(幂等过滤由后续案例专门处理,这里只做丢弃标记)。

缺口填补的判定条件是:当某条消息使 nextSeq 推进到超过缺口右端时,该缺口即被“闭合”,可从 gaps 中移除并记录闭合耗时。

到达序列(乱序) 101 103 105 102 104 106 ← 时间 乱序缓冲队列 buffer nextSeq = 101 101 ✓ 103 暂存 105 暂存 106 暂存 缺口记录 gaps = [102, 104] 102、104 到达后 → 连续释放 102→103→104→105→106 按序应用 101 → 订单簿 102 → 订单簿 103 → 订单簿 连续释放 缺口生命周期 ① 103 到达:nextSeq=101,检测缺口 [101,102],103 入 buffer ② 102 到达:填补 102,连续释放 102、103,nextSeq 推进到 104 ③ 104 到达:释放 104、105、106,缺口全部闭合,恢复实时更新 关键判据 seq == nextSeq → 应用 seq > nextSeq → 记缺口 seq < nextSeq → 丢弃

可运行的缓冲重排实现

下面用 Python 实现一个最小但完整的 ReorderBuffer,包含缺口记录、连续释放和闭合统计。示例消息序列与上图一致,便于对照观察。

from dataclasses import dataclass, field
from typing import Dict, List, Tuple

@dataclass
class IncrementalMsg:
    seq: int
    payload: str  # 实际为订单簿增量指令,这里用字符串占位

@dataclass
class ReorderBuffer:
    next_seq: int
    buffer: Dict[int, IncrementalMsg] = field(default_factory=dict)
    gaps: List[Tuple[int, int]] = field(default_factory=list)  # 未闭合缺口 [start, end]
    applied: List[int] = field(default_factory=list)
    dropped: List[int] = field(default_factory=list)
    closed_gaps: List[Tuple[int, int]] = field(default_factory=list)

    def _close_gaps(self):
        """next_seq 推进后,移除右端已被覆盖的缺口"""
        still_open = []
        for start, end in self.gaps:
            if self.next_seq > end:
                self.closed_gaps.append((start, end))
            else:
                still_open.append((start, end))
        self.gaps = still_open

    def _drain(self):
        """从 next_seq 开始连续释放 buffer 中的消息"""
        while self.next_seq in self.buffer:
            msg = self.buffer.pop(self.next_seq)
            self.applied.append(msg.seq)
            self.next_seq += 1
        self._close_gaps()

    def push(self, msg: IncrementalMsg) -> str:
        if msg.seq < self.next_seq:
            self.dropped.append(msg.seq)
            return "dropped"
        if msg.seq == self.next_seq:
            self.applied.append(msg.seq)
            self.next_seq += 1
            self._drain()
            return "applied"
        # msg.seq > next_seq:存在缺口
        if not self.gaps or self.gaps[-1][1] < msg.seq - 1:
            self.gaps.append((self.next_seq, msg.seq - 1))
        self.buffer[msg.seq] = msg
        return "buffered"

# ---- 示例:与图中到达顺序一致 ----
rb = ReorderBuffer(next_seq=101)
arrivals = [101, 103, 105, 102, 104, 106]
for s in arrivals:
    action = rb.push(IncrementalMsg(seq=s, payload=f"delta-{s}"))
    print(f"seq={s:3d} -> {action:8s} next_seq={rb.next_seq} "
          f"gaps={rb.gaps} buffer={sorted(rb.buffer)}")

print("applied:", rb.applied)
print("dropped:", rb.dropped)
print("closed_gaps:", rb.closed_gaps)

预期输出(可直接由上述输入推出):

seq=101 -> applied  next_seq=102 gaps=[] buffer=[]
seq=103 -> buffered next_seq=102 gaps=[(102, 102)] buffer=[103]
seq=105 -> buffered next_seq=102 gaps=[(102, 102), (104, 104)] buffer=[103, 105]
seq=102 -> applied  next_seq=106 gaps=[] buffer=[]
seq=104 -> dropped  next_seq=106 gaps=[] buffer=[]
seq=106 -> dropped  next_seq=106 gaps=[] buffer=[]
applied: [101, 102, 103, 104, 105, 106]
dropped: []
closed_gaps: [(102, 102), (104, 104)]

注意:上面输出中 seq=104 与 seq=106 显示为 dropped,是因为 102 到达时 _drain() 已把 buffer 中的 103、105 连同 104、106 一并释放(104、106 在 102 到达前已入 buffer)。这说明缺口填补会触发级联释放,后续到达的同号消息自然成为重复消息被丢弃。若希望区分“重复”与“过期”,可在 dropped 中记录原因。

缺口检测的边界与验证要点

实现中有三个容易出错的边界,建议在单元测试中逐一覆盖:

场景输入期望行为
首条即缺口nextSeq=101,先到 103记录缺口 [101,102],103 入 buffer,不应用任何消息
缺口被单条填补缺口 [102,102],随后到 102释放 102 及 buffer 中连续后继,缺口闭合
缺口跨多条填补缺口 [102,104],依次到 102、103、104每步推进 nextSeq,最后一次性闭合缺口
重复消息nextSeq=106,到 104丢弃并计数,nextSeq 不变

工程上还应给 buffer 设置容量上限与超时:若缺口长时间未填补(例如超过 2 秒或 buffer 超过 500 条),应放弃等待并触发快照重建,而不是无限缓存。缺口闭合耗时(closed_gaps 的时间戳差)是判断链路质量的重要指标,建议与序列号跳变告警一起上报。

验证方法:用上面的示例序列跑一遍,确认 applied 严格等于 [101,102,103,104,105,106],且 closed_gaps 覆盖所有曾检测到的缺口。再构造“缺口永不填补”的序列(如 101、103、105 后不再有 102),确认 gaps 持续存在且 next_seq 停在 102,此时上层应转入快照重建流程。