内存模型设计:Level2快照与增量、红黑树与跳表在订单簿中的应用

好,咱们接着聊订单簿重建的核心——内存模型设计。

说实话,这块是我当年踩坑最多的部分。刚开始做量化系统时,我以为订单簿就是个简单的价格队列,用个HashMap就能搞定。结果一上生产环境,Level2行情一来,直接把我那点小聪明打得粉碎。

今天咱们就掰开揉碎了讲:快照与增量怎么配合,红黑树跳表到底谁更适合订单簿。

一、Level2快照与增量:一对欢喜冤家

先说说为什么需要快照和增量两套机制。

你想想看,交易所每秒发几千笔成交,每笔都带完整的订单簿?那带宽早爆了。所以实际做法是:定期发一个全量快照,中间只发增量变化

核心原则:快照用于重建基准,增量用于实时更新。两者缺一不可。

1.1 快照设计

快照说白了就是某一时刻订单簿的完整拷贝。我习惯用内存映射文件来存,这样重启时能秒级恢复。

快照里要存什么?

  • 时间戳(精确到微秒)
  • 买卖十档的价格和数量
  • 当前序列号(用于和增量对齐)
// 快照结构体示例
struct Snapshot {
    int64_t timestamp;      // 微秒时间戳
    int64_t seq_no;         // 交易所序列号
    PriceLevel bids[10];    // 买十档
    PriceLevel asks[10];    // 卖十档
};

我在项目中遇到过一个问题:快照生成太频繁,磁盘I/O扛不住。后来改成每100毫秒生成一次,配合增量日志,效果刚刚好。

1.2 增量设计

增量就是订单簿的变化事件。常见的增量类型有:

  • 新增订单:某个价格档位出现新单
  • 撤单:某个订单被撤销
  • 成交:订单部分或全部成交
  • 修改:订单价格或数量变更

避坑指南:我曾经把增量日志设计成单线程写入,结果行情爆发时写入延迟导致丢数据。后来改成双缓冲+批量刷盘,问题才解决。

二、红黑树 vs 跳表:订单簿的骨架

好,现在进入正题。订单簿本质上是个有序映射:价格作为key,订单队列作为value。那么问题来了——用什么数据结构来维护这个映射?

我做过对比测试,直接上结果:

特性 红黑树 跳表
查找时间复杂度 O(log n) O(log n) 期望
插入/删除 O(log n),需要旋转 O(log n),无旋转
范围查询 中序遍历 O(n) 链表遍历 O(n)
内存占用 较低(2个指针) 较高(平均4-6个指针)
并发友好度 差(全局锁) 好(可分段加锁)

2.1 红黑树:老牌劲旅

红黑树是C++ std::map的标配。它的优势在于稳定——最坏情况也是O(log n)。

但有个问题:插入和删除需要旋转。在订单簿场景下,每秒可能有几万次价格变动,旋转操作会带来不小的开销。

// 红黑树实现订单簿示例
std::map<double, OrderQueue, std::greater<double>> bid_book;
std::map<double, OrderQueue> ask_book;

// 插入买单
void insert_bid(double price, Order order) {
    bid_book[price].push_back(order);
    // 红黑树自动维护有序性
}

注意:红黑树在高并发写入场景下容易成为瓶颈。我见过一个系统,因为红黑树加锁粒度太粗,导致行情处理延迟从50微秒飙升到2毫秒。

2.2 跳表:后起之秀

跳表是我个人比较偏爱的选择。它用概率平衡代替了红黑树的严格平衡,实现起来简单得多。

跳表最大的优势是无锁并发。你可以用CAS操作实现无锁插入,这在订单簿场景下简直是神器。

// 跳表节点结构
struct SkipNode {
    double price;
    OrderQueue queue;
    std::vector<SkipNode*> next;  // 多层指针
};

// 插入操作(无锁版本)
void insert_skip(SkipNode* head, double price, Order order) {
    // 用CAS实现无锁插入
    SkipNode* new_node = new SkipNode(price, order);
    // ... 省略CAS循环细节
}

三、我的选择:混合策略

说了这么多,到底用哪个?

我的建议是:别二选一,混合着用

  • 快照重建:用红黑树。因为快照是批量加载,不需要高并发,红黑树的内存效率更高。
  • 实时增量:用跳表。因为增量是高频写入,跳表的无锁特性可以大幅降低延迟。

实战经验:我在一个期货高频系统里,把快照层用红黑树,增量层用跳表。结果延迟从平均800纳秒降到了300纳秒。嗯,效果立竿见影。

四、内存模型整体架构

最后,咱们用一张图来总结整个内存模型的设计思路:

订单簿内存模型架构 数据源层 交易所行情流(快照 + 增量) 协议解析层 解码、校验、序列号对齐 内存模型层 快照区(红黑树) 全量订单簿,每100ms更新 增量区(跳表) 实时变化,无锁并发写入 应用层 策略引擎、风控模块、监控系统

这张图把整个流程串起来了。数据从交易所进来,经过解析层,然后分别进入快照区和增量区。策略引擎从这两个区读取数据,构建出完整的订单簿视图。

个人建议:快照区和增量区之间要有个对齐机制。我习惯用序列号来对齐——每次快照生成时记录当前序列号,增量日志从该序列号之后开始回放。这样即使快照和增量之间有延迟,也能保证数据一致性。

好了,内存模型这块就聊到这儿。记住一句话:没有银弹。红黑树和跳表各有优劣,关键看你的业务场景。快照用红黑树省内存,增量用跳表提性能——这是我踩了无数坑之后总结出来的经验。


专注资料整理