3、增量更新机制:Snapshot与Incremental的区别、Sequence Number的作用、应用层去重
好,咱们来聊聊订单簿重建里最核心的一个话题——增量更新。
说实话,我刚入行那会儿,觉得订单簿嘛,不就是维护一个价格优先的队列?后来真正做低延迟系统才发现,怎么高效地把订单簿从零重建出来,才是真正的技术活。
3.1 Snapshot vs Incremental:两种思路的较量
先问个问题:如果你要恢复一个订单簿,你会怎么做?
最简单的办法——把当前所有挂单全拉下来。这就是全量快照(Snapshot)。
另一种思路——只告诉你「刚才发生了什么变化」。这就是增量更新(Incremental)。
我个人的习惯是:两者结合,缺一不可。为什么?
核心区别一览
| 维度 | Snapshot | Incremental |
|---|---|---|
| 数据量 | 大(全量订单) | 小(仅变化部分) |
| 重建速度 | 快(一次到位) | 慢(需逐条回放) |
| 带宽消耗 | 高 | 低 |
| 容错性 | 强(丢包也不怕) | 弱(丢一条就乱) |
| 典型频率 | 每1-10秒一次 | 每次订单变动都发 |
你看,Snapshot 就像拍一张照片,Incremental 就像放一段录像。照片清晰但占空间,录像省空间但需要连续播放。
我在项目中遇到过一个问题:某交易所的增量更新频率极高,每秒几千条。如果只靠增量重建,一旦中间丢了一条,整个订单簿就全乱了。后来我们改成每5秒拉一次快照,中间用增量补,问题就解决了。
3.2 Sequence Number:增量更新的「安全带」
增量更新有个致命弱点——丢包。
你想想看,如果增量消息是「第1笔:新增买单A,第2笔:撤销买单A」,结果第1笔丢了,你只收到第2笔。这时候你会去撤销一个根本不存在的订单……订单簿就崩了。
怎么解决?Sequence Number(序列号)。
说白了,每条增量消息都带一个递增的序号。接收方检查序号是否连续,如果不连续,就知道丢包了,需要重新请求快照。
我的实践经验
我建议序列号用 uint64_t,别用 int。为什么?因为增量消息量巨大,int 可能溢出。我曾经见过一个系统用 uint32_t,运行了几个月后序列号回绕,导致去重逻辑全乱了……
序列号的作用其实就三个:
- 检测丢包:当前序号 != 上次序号 + 1,说明丢了
- 保证顺序:网络可能乱序,按序号排序后再处理
- 去重基础:同一个序号只处理一次
嗯,这里要注意:序列号不能只靠交易所给。我习惯在应用层再维护一个本地序列号,双重校验更安全。
3.3 应用层去重:别让重复消息害了你
网络环境不是完美的。同一个增量消息可能被发送两次(比如 TCP 重传、网关重试)。如果你不处理重复,订单簿里就会出现两个相同的订单。
去重的核心逻辑很简单:
// 伪代码:应用层去重
class OrderBookReconstructor {
uint64_t last_seq_ = 0;
std::unordered_set<uint64_t> processed_seqs_;
void OnIncremental(const IncrementalMsg& msg) {
// 1. 检查序列号连续性
if (msg.seq != last_seq_ + 1) {
// 丢包了,请求快照
RequestSnapshot();
return;
}
// 2. 检查是否重复
if (processed_seqs_.count(msg.seq)) {
// 已处理过,跳过
return;
}
// 3. 处理增量
ApplyIncremental(msg);
// 4. 更新状态
last_seq_ = msg.seq;
processed_seqs_.insert(msg.seq);
// 5. 清理旧记录(防止内存泄漏)
if (processed_seqs_.size() > 10000) {
// 保留最近10000个序号
CleanOldSeqs();
}
}
};
这段代码看着简单,但有几个坑:
- 内存泄漏:
processed_seqs_会无限增长,必须定期清理 - 性能:
unordered_set的查找是 O(1),但哈希冲突多了会退化 - 边界情况:快照后序列号重置,需要清空
processed_seqs_
我曾经踩过的坑
有一次,交易所的序列号在快照后没有重置,而是继续递增。我们的去重逻辑以为序号不连续,疯狂请求快照,导致系统雪崩。后来我加了一个判断:收到快照后,清空所有去重记录,重新开始计数。
3.4 整体流程:一张图说清楚
下面这张图展示了订单簿重建的完整流程,包括快照、增量、序列号和去重的关系:
从这张图你可以看到:快照是基础,增量是日常,序列号是保障,去重是兜底。四者缺一不可。
3.5 实战建议:怎么选?
最后给几个我个人的建议:
- 高频交易场景:增量为主,快照为辅。快照频率可以低一些(比如5秒一次),减少带宽消耗
- 低延迟场景:增量消息的解析要极致优化。我习惯用
memcpy直接解析二进制流,不用 protobuf - 容错优先场景:快照频率高一些(比如1秒一次),丢包后能快速恢复
- 去重记录别用
std::set:查找是 O(log n),量大了扛不住。用unordered_set或者环形缓冲区
一句话总结
增量更新不是银弹,序列号不是万能药。只有把快照、增量、序列号、去重四者配合好,才能构建一个稳定高效的订单簿重建系统。
公众号:蓝海资料掘金营,微信deep3321