4. 序列号跳变时的订单簿重建触发策略

跳变检测的核心判断维度

序列号跳变与普通乱序不同:乱序是消息迟到但最终会到,跳变意味着中间一段序列号可能永久缺失。处理策略不能一刀切,需要同时看两个量:跳变幅度(缺口大小)和持续时间(缺口存在多久未被填补)。幅度小且短暂,通常只是网络抖动,等待补发即可;幅度大或持续超时,则说明数据源可能丢包或推送中断,必须请求新快照重建。

序列号跳变决策矩阵(幅度 × 持续时间) 跳变幅度(缺口消息数) 持续时间(ms) 小 (≤5) 中 (6~50) 大 (>50) 短 中 长 等待补发 缓冲增量,启动补发计时器 超时前缺口填补则继续 请求新快照重建 停止应用增量,发起快照请求 缓存后续增量待快照回放 观察窗口 短时等待,超时升级 为快照请求 幅度阈值 时间阈值 记录决策上下文 expectedSeq / receivedSeq gapSize / gapAgeMs decision / timestamp

检测逻辑实现

核心思路:维护一个expectedSeq(下一条应到达的序列号)。每收到一条增量消息,比较其序列号与expectedSeq。若相等则正常应用并递增;若大于则记录缺口,启动计时器;若小于则属于重复消息(由幂等过滤器处理,本例不展开)。缺口在计时器到期前被填补则清除,否则触发快照重建。

import time
from dataclasses import dataclass, field
from enum import Enum

class Decision(Enum):
    APPLY = "apply"
    WAIT = "wait"
    REBUILD = "rebuild"

@dataclass
class GapContext:
    expected_seq: int
    received_seq: int
    gap_size: int
    first_seen_ms: float
    decision: str = ""
    reason: str = ""

class SequenceGapDetector:
    def __init__(self, max_gap_size=50, max_gap_age_ms=2000):
        self.expected_seq = None
        self.max_gap_size = max_gap_size
        self.max_gap_age_ms = max_gap_age_ms
        self.active_gap: GapContext | None = None
        self.decision_log: list[GapContext] = []

    def on_message(self, seq: int, now_ms: float | None = None) -> Decision:
        now_ms = now_ms if now_ms is not None else time.monotonic() * 1000

        if self.expected_seq is None:
            self.expected_seq = seq + 1
            return Decision.APPLY

        if seq == self.expected_seq:
            # 缺口被填补
            if self.active_gap:
                self.active_gap.decision = "filled"
                self.decision_log.append(self.active_gap)
                self.active_gap = None
            self.expected_seq = seq + 1
            return Decision.APPLY

        if seq < self.expected_seq:
            return Decision.APPLY  # 重复消息,交给幂等层

        # seq > expected_seq:出现跳变
        gap_size = seq - self.expected_seq
        if self.active_gap is None:
            self.active_gap = GapContext(
                expected_seq=self.expected_seq,
                received_seq=seq,
                gap_size=gap_size,
                first_seen_ms=now_ms,
            )

        gap_age = now_ms - self.active_gap.first_seen_ms

        # 决策:幅度大 或 持续超时 → 重建
        if gap_size > self.max_gap_size or gap_age > self.max_gap_age_ms:
            self.active_gap.decision = "rebuild"
            self.active_gap.reason = (
                f"gap_size={gap_size}>{self.max_gap_size}"
                if gap_size > self.max_gap_size
                else f"gap_age={gap_age:.0f}ms>{self.max_gap_age_ms}ms"
            )
            self.decision_log.append(self.active_gap)
            self.active_gap = None
            return Decision.REBUILD

        return Decision.WAIT

    def tick(self, now_ms: float) -> Decision:
        """定时调用,检查活跃缺口是否超时"""
        if self.active_gap is None:
            return Decision.APPLY
        gap_age = now_ms - self.active_gap.first_seen_ms
        if gap_age > self.max_gap_age_ms:
            self.active_gap.decision = "rebuild"
            self.active_gap.reason = f"gap_age={gap_age:.0f}ms>{self.max_gap_age_ms}ms"
            self.decision_log.append(self.active_gap)
            self.active_gap = None
            return Decision.REBUILD
        return Decision.WAIT

验证与判读

用一组构造的序列号流验证三种路径:小缺口被填补、大缺口立即重建、小缺口超时升级重建。以下为教学示例输入与预期决策,非实测结果。

场景输入序列号时间(ms)预期决策记录上下文
正常连续100,101,1020,10,20APPLY ×3无缺口
小缺口填补103,106,104,10530,40,50,60APPLY,WAIT,APPLY,APPLYgap_size=3, decision=filled
大缺口重建107,20070,80APPLY,REBUILDgap_size=93, reason=gap_size>50
小缺口超时201,20390,100APPLY,WAITtick(2200) → REBUILD, reason=gap_age>2000ms
阈值选择建议:max_gap_size 应结合行情推送频率设定——高频场景(如逐笔委托)可设 20~50,低频场景可放宽。 max_gap_age_ms 通常取 1~3 秒,需小于快照请求的往返延迟,避免等待期间订单簿持续偏离。
重建触发后必须立即停止应用增量消息,否则会在错误基线上叠加更新。重建期间到达的增量应缓存,待快照回放时按序列号对齐。决策日志中的 expected_seq 与 received_seq 是事后排查丢包范围的关键依据,不可省略。