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 排序后再应用。

快照重建

增量更新虽然高效,但有个问题:如果中间丢了一条消息,整个订单簿就废了。所以需要定期发送全量快照。

快照重建的流程一般是:

  1. 请求当前快照(包含所有价格档位)
  2. 记录请求时的 seqNo
  3. 应用快照后,再应用 seqNo 之后的增量消息
  4. 如果中间又丢了消息,重新请求快照

我曾经踩过一个坑:快照生成和增量更新是并发的。生成快照时,订单簿还在变化,导致快照和增量之间有重叠或遗漏。后来我改用「双缓冲」方案:生成快照时先冻结一份副本,等快照生成完再切换。虽然多占一倍内存,但保证了数据一致性。

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 整体架构图

下面这张图展示了订单簿管理的核心流程。从原始数据到聚合深度,每一步都有明确的职责。

订单簿管理核心架构 交易所行情 跳表 / 红黑树 O(log n) 插入/删除/查询 增量更新 + 快照重建 seqNo 保证顺序,双缓冲保证一致性 深度合并 / 价格聚合 自适应 tickSize,按档位累计 策略引擎 / 风控

从图上你能看到,数据从交易所进来,先经过跳表/红黑树做基础存储,然后通过增量更新机制对外发布,最后聚合层把原始数据加工成策略能用的格式。每一层都有明确的边界和职责。

4.5 实战建议

  • 内存管理:订单簿数据量可能很大。我建议用对象池(Object Pool)来复用订单对象,避免频繁 GC。Go 语言里可以用 sync.Pool,C++ 可以用 boost::pool。
  • 性能监控:订单簿的插入延迟、快照生成时间、聚合耗时,都要打点监控。我习惯用 Prometheus + Grafana 做可视化,一眼就能看出瓶颈在哪。
  • 容错设计:增量消息可能重复或丢失。接收方要做幂等处理——同一个 seqNo 的消息只应用一次。可以用布隆过滤器快速去重。

一句话总结:订单簿管理就是「存得快、更新准、看得清」。数据结构选跳表,增量更新用 seqNo 保序,深度聚合用自适应 tickSize。这三板斧用好了,你的做市系统就稳了一半。

专注资料整理