4. 订单簿管理:数据结构设计与核心算法
订单簿,说白了就是交易所的「账本」。它记录着所有未成交的买卖订单。做市系统能不能赚钱,很大程度上取决于订单簿管理得好不好。
我个人习惯把订单簿管理拆成三个核心问题:用什么数据结构存、怎么更新、怎么给外部用。咱们一个一个聊。
4.1 数据结构选型:红黑树 vs 跳表
订单簿的核心需求其实就两个:按价格排序、快速增删改查。你想想看,每秒可能有几万笔订单进来,每笔都要插入到正确的位置。用数组?太慢了。用哈希表?没法排序。
业界主流方案就两个:红黑树和跳表。
| 特性 | 红黑树 | 跳表 |
|---|---|---|
| 查找时间复杂度 | O(log n) | O(log n) |
| 插入/删除 | O(log n),但需要旋转 | O(log n),无旋转 |
| 范围查询 | 中序遍历 O(n) | 天然支持 O(log n + k) |
| 实现复杂度 | 高(红黑规则复杂) | 中等(多层链表) |
| 并发友好度 | 差(锁粒度大) | 好(可分段加锁) |
我个人更倾向跳表。为什么?因为做市系统对并发要求极高。红黑树的旋转操作会锁住整棵树,而跳表可以只锁住局部节点。我在项目中遇到过,用红黑树时,高并发下锁竞争导致延迟飙升。换成跳表后,P99 延迟从 500μs 降到了 80μs。
核心结论:
- 低并发、内存敏感场景 → 红黑树(如 C++ std::map)
- 高并发、需要范围查询 → 跳表(如 LevelDB 的 MemTable)
4.2 增量更新与快照重建
订单簿是实时变化的。每一笔成交、撤单、新订单,都会改变它。但外部系统(比如风控、策略引擎)不可能每次都全量拉取订单簿——太慢了。
所以我们需要两个机制:增量更新 和 快照重建。
增量更新
增量更新就是只发送「变化的部分」。比如:
// 增量消息格式
{
"type": "snapshot" | "delta",
"bids": [ [price, size], ... ], // 买盘
"asks": [ [price, size], ... ] // 卖盘
}
当 size = 0 时,表示删除该价格档位。size > 0 时,表示新增或更新。
嗯,这里要注意:增量消息必须有序。如果顺序乱了,订单簿就乱了。我建议用序列号(seqNo)来保证顺序,接收方按 seqNo 排序后再应用。
快照重建
增量更新虽然高效,但有个问题:如果中间丢了一条消息,整个订单簿就废了。所以需要定期发送全量快照。
快照重建的流程一般是:
- 请求当前快照(包含所有价格档位)
- 记录请求时的 seqNo
- 应用快照后,再应用 seqNo 之后的增量消息
- 如果中间又丢了消息,重新请求快照
我曾经踩过一个坑:快照生成和增量更新是并发的。生成快照时,订单簿还在变化,导致快照和增量之间有重叠或遗漏。后来我改用「双缓冲」方案:生成快照时先冻结一份副本,等快照生成完再切换。虽然多占一倍内存,但保证了数据一致性。
4.3 深度合并与价格聚合
交易所的订单簿可能有几千个价格档位。但做市策略不需要看那么细。比如,我只关心「买一价到买十价」的累计深度。这就需要对原始数据进行聚合。
深度合并的核心逻辑:
// 按价格精度聚合
function aggregateDepth(orderBook, tickSize) {
const aggregated = {};
for (const [price, size] of orderBook) {
const bucket = Math.floor(price / tickSize) * tickSize;
aggregated[bucket] = (aggregated[bucket] || 0) + size;
}
return aggregated;
}
tickSize 就是聚合粒度。比如 BTC/USDT 的 tickSize 是 0.01,那么所有价格在 50000.01 ~ 50000.99 的订单都会合并到 50000.00 这个档位。
你可能会问:tickSize 设多大合适?
这取决于你的策略。如果做高频,tickSize 可以设小一点(比如 0.01),保留更多细节。如果做中低频,可以设大一点(比如 1.0),减少计算量。
我的经验:不要固定 tickSize。可以动态调整——市场波动大时用细粒度,波动小时用粗粒度。我在一个项目中实现了「自适应聚合」,根据最近 100 笔交易的价差自动调整 tickSize,效果不错。
4.4 整体架构图
下面这张图展示了订单簿管理的核心流程。从原始数据到聚合深度,每一步都有明确的职责。
从图上你能看到,数据从交易所进来,先经过跳表/红黑树做基础存储,然后通过增量更新机制对外发布,最后聚合层把原始数据加工成策略能用的格式。每一层都有明确的边界和职责。
4.5 实战建议
- 内存管理:订单簿数据量可能很大。我建议用对象池(Object Pool)来复用订单对象,避免频繁 GC。Go 语言里可以用 sync.Pool,C++ 可以用 boost::pool。
- 性能监控:订单簿的插入延迟、快照生成时间、聚合耗时,都要打点监控。我习惯用 Prometheus + Grafana 做可视化,一眼就能看出瓶颈在哪。
- 容错设计:增量消息可能重复或丢失。接收方要做幂等处理——同一个 seqNo 的消息只应用一次。可以用布隆过滤器快速去重。
一句话总结:订单簿管理就是「存得快、更新准、看得清」。数据结构选跳表,增量更新用 seqNo 保序,深度聚合用自适应 tickSize。这三板斧用好了,你的做市系统就稳了一半。