第二章:核心数据结构设计——订单簿的深度解析

做双边报价系统,最核心的东西是什么?

我个人觉得,就是订单簿(Order Book)。你想想看,所有的买卖决策、价格发现、风险控制,最终都要落到这个数据结构上。今天我们就把它彻底拆开,看看里面到底藏着什么门道。

2.1 订单簿的本质:价格-数量队列

订单簿说白了,就是一个「价格-数量」的映射关系。买盘(Bid)按价格从高到低排,卖盘(Ask)按价格从低到高排。每个价格上挂了多少量,一目了然。

我在项目中遇到过一个问题:刚开始做的时候,直接用了个简单的HashMap,key是价格,value是数量。结果呢?撮合的时候要遍历找最优价格,性能直接崩了。嗯,这里要注意——订单簿不是简单的KV存储,它需要有序性。

核心要点:订单簿必须同时支持三种操作——插入、删除、范围查询。而且这些操作都要在微秒级完成。

我们来看一个典型的Level 2数据模型:

// 买盘(Bid) - 价格降序排列
Price  |  Quantity  |  Order Count
100.50 |    2,000   |     12
100.49 |    1,500   |      8
100.48 |    3,200   |     15
...

// 卖盘(Ask) - 价格升序排列
Price  |  Quantity  |  Order Count
100.51 |    1,800   |     10
100.52 |    2,500   |     14
100.53 |    1,200   |      7
...

每个价格档位(Price Level)上,可能挂着多个订单。这就是所谓的「价格-时间优先」原则——同价格先来的先成交。

2.2 Level 2 数据模型的深度拆解

Level 2 数据,也叫市场深度数据。它比普通的买卖五档要详细得多。我习惯把它分成三层:

层级 内容 更新频率
Level 1 最优买卖价、最新成交价 事件驱动
Level 2 所有价格档位的挂单量 每次订单变动
Level 3 每个订单的详细信息 实时逐笔

做双边报价系统,Level 2 是底线。你至少要知道每个价格上有多少对手盘。我曾经见过一个团队,只用了Level 1数据做报价,结果遇到大单扫货,报价直接被击穿,亏了不少钱。

避坑指南:Level 2 数据量很大。一个活跃的币对,每秒可能有上千次价格档位变化。如果每次变化都全量推送,带宽和CPU都扛不住。我建议用增量更新(Snapshot + Incremental Update)的方式。

2.3 内存数据结构选型:红黑树 vs 跳表 vs 哈希表

这是面试高频题,也是实战中的关键决策。我们来逐个分析:

2.3.1 哈希表(HashMap)

查价格快,O(1)。但问题是——它不支持有序遍历。你要找最优买卖价,得遍历所有key。这在订单簿场景下基本不可用。除非你只做简单的价格查询,否则别用它做主数据结构。

2.3.2 红黑树(Red-Black Tree)

C++里用 std::map,Java里用 TreeMap。插入、删除、查找都是 O(log n)。而且天然有序,可以快速找到最优价格。我在早期项目中用的就是红黑树。

但红黑树有个问题——内存开销大。每个节点要存颜色、父节点指针、左右子节点指针。一个订单档位可能就占几十个字节。而且在高并发场景下,红黑树的旋转操作会引发锁竞争。

2.3.3 跳表(Skip List)

我个人现在更倾向于跳表。为什么?

  • 实现简单,不容易出bug
  • 范围查询性能好,可以快速遍历价格区间
  • 无锁化改造相对容易
  • 内存分配更友好,节点可以复用

你看这个对比表就清楚了:

特性 红黑树 跳表 哈希表
插入/删除 O(log n) O(log n) O(1)
有序遍历 支持 支持 不支持
范围查询 O(log n + k) O(log n + k) O(n)
实现复杂度
无锁化难度

我的建议:如果团队C++功底扎实,用红黑树没问题。如果追求开发效率和可维护性,跳表是更好的选择。哈希表只适合做辅助索引,比如用订单ID查订单详情。

2.4 实战中的数据结构组合

实际项目中,我不会只用一种数据结构。我习惯用「组合拳」:

// 订单簿核心结构(伪代码)
class OrderBook {
    // 主索引:价格 -> 订单列表(用跳表实现)
    SkipList<Price, OrderList> bidLevels;  // 买盘
    SkipList<Price, OrderList> askLevels;  // 卖盘
    
    // 辅助索引:订单ID -> 订单详情(用哈希表)
    HashMap<OrderId, Order> orderMap;
    
    // 缓存:最优买卖价(用原子变量)
    AtomicReference<Price> bestBid;
    AtomicReference<Price> bestAsk;
}

为什么要加辅助索引?因为撤单的时候,你得根据订单ID快速找到它在哪个价格档位上。没有哈希表辅助,你得遍历跳表,那就慢了。

我曾经踩过一个坑:一开始没加缓存,每次查询最优价都要去跳表里找。结果在每秒10万笔订单的压力下,CPU全耗在跳表遍历上了。后来加了原子变量缓存最优价,性能直接提升了3倍。

2.5 知识体系总览

下面这张图,是我对订单簿数据结构设计的理解。你可以把它当作一个思维导图:

订单簿数据结构 价格-数量队列 Level 2 数据模型 内存数据结构选型 买盘(Bid)降序 卖盘(Ask)升序 增量更新机制 快照+增量 红黑树 跳表 哈希表 O(log n) 操作 内存开销大 易实现、易无锁化 范围查询友好 核心原则:有序性 + 高性能 + 低延迟 组合使用多种数据结构,各取所长

这张图把今天讲的内容串起来了。你仔细看,核心就三个字——有序性。没有有序性,订单簿就是一堆散沙。

总结一下:订单簿设计没有银弹。红黑树、跳表、哈希表各有适用场景。我的经验是——用跳表做主索引,哈希表做辅助索引,原子变量做缓存。这套组合在大多数场景下都能跑得很好。

好了,这一章就到这里。记住,数据结构选型决定了系统的天花板。选对了,后面事半功倍;选错了,后面全是坑。

专注资料整理