1. 订单簿快照与增量消息的时序对齐校验
为什么快照与增量必须做序列号对齐
交易所的订单簿行情通常由两条独立通道组成:一条是低频的快照(Snapshot),携带某一时刻的完整盘口和该时刻的最后更新序列号 lastUpdateId;另一条是高频的增量消息(Diff/Update),每条携带首尾序列号 U(first)与 u(last)。重建订单簿时,必须保证增量消息的序列号区间与快照版本严格衔接,否则会出现漏单或重复应用,导致盘口深度错误。
对齐的核心判据只有一条:第一条被应用的增量消息必须满足 U ≤ lastUpdateId + 1 ≤ u。若某条增量的 u ≤ lastUpdateId,说明它已被快照覆盖,应丢弃;若某条增量的 U > lastUpdateId + 1,说明快照与增量之间存在缺口,快照版本过旧,必须重新拉取快照。校验器要做的就是在应用前逐条判定,并输出可诊断的异常信息。
校验器的数据结构与核心判定
校验器需要维护三个状态:快照版本 lastUpdateId、当前已应用到订单簿的序列号 currentSeq、以及是否已完成首次对齐的布尔标志 aligned。首次对齐前,所有增量都按快照版本判定;对齐后,则按 currentSeq 判定连续性。下面给出一个可直接运行的 Python 实现,包含诊断信息输出。
from dataclasses import dataclass, field
from typing import List, Optional
@dataclass
class DiffMessage:
U: int # 首序列号
u: int # 尾序列号
bids: list = field(default_factory=list)
asks: list = field(default_factory=list)
@dataclass
class Diagnostic:
level: str # INFO / WARN / ERROR
code: str # COVERED / ALIGNED / GAP / OVERLAP
detail: str
class SequenceAligner:
def __init__(self, snapshot_last_update_id: int):
self.last_update_id = snapshot_last_update_id
self.current_seq = snapshot_last_update_id
self.aligned = False
self.diagnostics: List[Diagnostic] = []
def check(self, msg: DiffMessage) -> Optional[Diagnostic]:
# 1. 已被快照覆盖
if msg.u <= self.last_update_id:
d = Diagnostic("INFO", "COVERED",
f"seq {msg.U}-{msg.u} 已被快照 {self.last_update_id} 覆盖,丢弃")
self.diagnostics.append(d)
return d
# 2. 首次对齐判定
if not self.aligned:
if msg.U <= self.last_update_id + 1 <= msg.u:
self.aligned = True
self.current_seq = msg.u
d = Diagnostic("INFO", "ALIGNED",
f"首条增量 seq {msg.U}-{msg.u} 与快照 {self.last_update_id} 衔接成功")
self.diagnostics.append(d)
return d
else:
d = Diagnostic("ERROR", "GAP",
f"快照 {self.last_update_id} 与增量首号 {msg.U} 之间存在缺口 "
f"{self.last_update_id + 1}-{msg.U - 1},需重拉快照")
self.diagnostics.append(d)
return d
# 3. 对齐后的连续性检查
if msg.U <= self.current_seq + 1 <= msg.u:
self.current_seq = msg.u
d = Diagnostic("INFO", "ALIGNED",
f"seq {msg.U}-{msg.u} 连续衔接,currentSeq 更新为 {self.current_seq}")
self.diagnostics.append(d)
return d
elif msg.U > self.current_seq + 1:
d = Diagnostic("ERROR", "GAP",
f"seq {msg.U}-{msg.u} 与 currentSeq {self.current_seq} 之间存在缺口 "
f"{self.current_seq + 1}-{msg.U - 1}")
self.diagnostics.append(d)
return d
else:
d = Diagnostic("WARN", "OVERLAP",
f"seq {msg.U}-{msg.u} 与 currentSeq {self.current_seq} 重叠,需幂等过滤")
self.diagnostics.append(d)
return d
用示例数据验证判定结果
假设快照 lastUpdateId = 1000,依次送入三条增量消息,观察校验器的判定与诊断输出。示例数据为教学构造,结果由上述逻辑直接推出。
aligner = SequenceAligner(snapshot_last_update_id=1000)
msgs = [
DiffMessage(U=980, u=995), # 已被快照覆盖
DiffMessage(U=996, u=1005), # 与快照衔接
DiffMessage(U=1010, u=1020), # 存在缺口 1006-1009
]
for m in msgs:
d = aligner.check(m)
print(f"[{d.level}] {d.code}: {d.detail}")
print("aligned =", aligner.aligned)
print("currentSeq =", aligner.current_seq)
预期输出(由给定输入直接推出):
[INFO] COVERED: seq 980-995 已被快照 1000 覆盖,丢弃
[INFO] ALIGNED: 首条增量 seq 996-1005 与快照 1000 衔接成功
[ERROR] GAP: seq 1010-1020 与 currentSeq 1005 之间存在缺口 1006-1009
aligned = True
currentSeq = 1005
u ≤ lastUpdateId 的消息会被误判为缺口,导致不必要的快照重拉。异常场景的诊断信息设计
诊断信息要能让运维或开发人员一眼定位问题类型与缺口范围。建议每条诊断至少包含:级别、错误码、涉及的序列号区间、当前基准序列号、以及建议动作。下表给出四类典型场景的判定与处置。
| 场景 | 判定条件 | 错误码 | 建议动作 |
|---|---|---|---|
| 增量已被快照覆盖 | u ≤ lastUpdateId | COVERED | 丢弃,继续下一条 |
| 首条增量与快照衔接 | U ≤ lastUpdateId+1 ≤ u | ALIGNED | 应用并置 aligned=True |
| 快照版本过旧 | U > lastUpdateId+1 | GAP | 重拉快照,重置校验器 |
| 增量区间重叠 | u ≤ currentSeq 且未覆盖 | OVERLAP | 幂等过滤,仅应用新部分 |
lastUpdateId 重建校验器,并把 aligned 重置为 False。若沿用旧 currentSeq,后续增量会被误判为重叠或缺口,订单簿将无法恢复一致。验证校验器是否按预期工作
用断言把上述示例固化为可重复执行的测试,覆盖“覆盖丢弃”“首次衔接”“缺口报错”三条路径,并检查 currentSeq 未被缺口消息污染。
def test_aligner():
a = SequenceAligner(1000)
d1 = a.check(DiffMessage(980, 995))
assert d1.code == "COVERED" and not a.aligned
d2 = a.check(DiffMessage(996, 1005))
assert d2.code == "ALIGNED" and a.aligned and a.current_seq == 1005
d3 = a.check(DiffMessage(1010, 1020))
assert d3.code == "GAP" and a.current_seq == 1005 # 缺口不推进基准
# 缺口补齐后应恢复
d4 = a.check(DiffMessage(1006, 1015))
assert d4.code == "ALIGNED" and a.current_seq == 1015
test_aligner()
print("all assertions passed")
通过条件:四条断言全部成立,且 currentSeq 在缺口消息后仍为 1005,说明校验器没有把不连续的消息错误地应用到订单簿基准上。若断言失败,优先检查判定顺序与 lastUpdateId + 1 的边界是否写成了 lastUpdateId。