4、增量更新机制:理解Event-driven架构、Snapshot + Update模式、Sequence Number校验
做订单簿重建,最怕什么?
我最怕的是数据对不上。你想想看,行情数据哗哗地流进来,如果每次都是全量快照,那带宽和延迟都扛不住。但只用增量更新吧,又怕漏掉一条,整个订单簿就歪了。
这就像拼乐高。你手里有一张完整的图纸(快照),然后不断收到“加一块红色2x4砖”、“拆掉蓝色1x2砖”这样的指令(更新)。只要漏掉一条指令,最后拼出来的东西就跟图纸不一样了。
今天我们就来聊聊,怎么用Event-driven架构、Snapshot + Update模式、还有Sequence Number校验,把这套机制做得既快又稳。
4.1 Event-driven架构:让数据自己跑起来
传统的做法是轮询。你每隔几毫秒去问一下:“有新数据吗?” 这效率太低了。大部分时候你都在空转。
Event-driven就不一样了。数据来了,它主动通知你。你只需要等着就行。
核心思想: 系统对“事件”做出响应,而不是主动去“拉取”数据。
在订单簿场景里,事件就是一条行情消息。可能是新增订单、撤单、或者成交。每个事件都携带着改变订单簿状态所需的信息。
我习惯把事件处理流程拆成三块:
- 事件源:交易所的行情推送接口。比如WebSocket或者FIX协议。
- 事件总线:一个轻量级的消息队列。我用过Redis的Pub/Sub,也用过ZeroMQ。关键是要快,延迟要低。
- 事件处理器:真正干活的地方。收到事件后,更新本地的订单簿数据结构。
这样做的好处很明显。各个模块解耦了。事件源只管发,处理器只管收。中间加个总线,还能做流量削峰。我曾经在实盘里遇到行情瞬间爆发,每秒几千条消息,全靠事件总线扛住了压力。
4.2 Snapshot + Update模式:又快又稳的秘密
全量快照太笨重,纯增量又不可靠。怎么办?
折中一下。用快照做基准,用增量做持续更新。
具体来说:
- 定期获取全量快照:比如每10秒或者每1分钟拉一次。快照里是当前时刻订单簿的完整状态。
- 在快照之间,只处理增量更新:每次收到一条增量的订单消息,就修改本地订单簿。
- 用快照做校验和修复:如果发现本地订单簿跟新快照对不上,就丢弃本地数据,用新快照重建。
这里有个关键点:快照和增量之间不能有缝隙。你拿到快照之后,必须确保快照之后的所有增量都处理了。否则就会丢数据。
我的经验: 快照频率不能太密,否则带宽扛不住。也不能太疏,否则增量累积太多,一旦出错,回退的成本很高。我个人习惯在流动性高的合约上设10秒一次快照,流动性差的设30秒。
下面这张图展示了Snapshot + Update模式的完整流程:
4.3 Sequence Number校验:别让数据溜走
有了快照和增量,还不够。你还需要一个东西来保证顺序和完整性。
这就是Sequence Number(序列号)。
每条行情消息,交易所都会给它一个递增的序号。比如第1001条、第1002条。你本地也维护一个变量,记录你最后处理的那条消息的序号。
校验逻辑很简单:
- 如果新消息的序号 = 本地序号 + 1,说明一切正常,处理它。
- 如果新消息的序号 > 本地序号 + 1,说明中间有消息丢了。这时候不能继续处理,必须请求新的全量快照来重建。
- 如果新消息的序号 <= 本地序号,说明是重复消息,直接丢弃。
注意: 千万不要在序号不连续的时候强行处理。我曾经在早期版本里跳过丢失的消息继续跑,结果订单簿价格差了整整两个tick,导致一笔套利交易直接亏损。从那以后,我对序号校验格外严格。
代码实现其实不复杂。我贴一段核心逻辑:
class OrderBookUpdater:
def __init__(self):
self.last_seq = 0
self.order_book = {}
self.need_snapshot = False
def process_update(self, msg):
seq = msg['sequence']
# 重复消息,直接忽略
if seq <= self.last_seq:
return
# 序号不连续,标记需要重建
if seq != self.last_seq + 1:
self.need_snapshot = True
print(f"序列号断裂: 期望 {self.last_seq + 1}, 收到 {seq}")
return
# 正常处理增量
self._apply_update(msg)
self.last_seq = seq
def apply_snapshot(self, snapshot):
self.order_book = snapshot['data']
self.last_seq = snapshot['sequence']
self.need_snapshot = False
这段代码看着简单,但实际生产环境里,你还要考虑并发问题。比如多个线程同时处理消息,序号校验就可能乱掉。我一般用单线程的事件循环来处理,省心。
4.4 综合起来:一个完整的增量更新流程
把上面三块拼在一起,流程就清晰了:
- 系统启动时,先请求一次全量快照,拿到基准状态和最新的序列号。
- 之后持续监听增量更新事件。每来一条,先校验序列号。
- 如果序号连续,就更新本地订单簿。
- 如果序号断裂,立即标记“需要重建”,并请求新的快照。
- 收到新快照后,用快照覆盖本地数据,重置序列号,继续处理增量。
这里有个小技巧。我建议在本地维护一个“待处理队列”。当序号断裂时,后续的增量消息不要直接丢弃,而是先缓存起来。等快照重建完成后,再根据新快照的序列号,判断哪些缓存的消息可以继续处理。这样可以减少数据丢失。
核心原则: 宁可慢一点,也要保证数据准确。订单簿错了,交易策略再牛也是白搭。
嗯,增量更新机制就聊到这里。Event-driven架构让你高效,Snapshot + Update模式让你兼顾速度和可靠性,Sequence Number校验则是最后的保险。这三者缺一不可。
你在实际开发中,可以根据交易所的API特性,调整快照频率和缓存策略。但核心逻辑,我建议不要轻易改动。毕竟,我踩过的坑,不想你再踩一遍。
公众号:蓝海资料掘金营,微信deep3321