2. 核心数据结构:订单簿(OrderBook)设计、价格队列(PriceQueue)、时间优先队列

好,咱们直接进入正题。上一章聊了撮合引擎的整体架构,说白了就是「谁来干活」。这一章,我们聊聊「活怎么干」——也就是数据怎么存、怎么查、怎么比。

我个人习惯,设计任何系统之前,先想清楚数据模型。订单簿就是撮合引擎的「内存数据库」,它的设计直接决定了撮合速度的上限。你想想看,每秒几万笔订单进来,如果查个最优价格都要遍历链表,那系统早就崩了。

2.1 订单簿的核心结构

订单簿,说白了就是两个队列:买单队列(Bid)和卖单队列(Ask)。但这里有个关键点——价格优先

  • 买单:价格高的排在前面。谁出价高,谁先成交。
  • 卖单:价格低的排在前面。谁要价低,谁先成交。

嗯,这里要注意:价格相同的时候怎么办?那就看谁先来——时间优先

所以订单簿的核心逻辑就是:价格优先 + 时间优先。这个组合,业内叫「价格-时间优先队列」。

核心原则:价格优先是第一优先级,时间优先是第二优先级。价格不同时,只看价格;价格相同时,才看时间。

我在项目中遇到过一个问题:有人把时间戳精度设成了毫秒,结果同一毫秒内来了两笔同价订单,系统直接随机排序了。嗯,这其实是个坑——时间优先必须保证全局唯一且递增。我建议用自增序列号,而不是系统时间戳。

2.2 价格队列(PriceQueue)设计

价格队列,就是按价格分组的订单列表。每个价格点,对应一个队列。比如买一价100.50元,下面挂了一堆订单。

为什么要这么设计?因为撮合的时候,我们只关心「最优价格」上的订单。如果每个价格都单独一个队列,那查找最优价格就变成了查红黑树或跳表——O(log n)搞定。

我常用的数据结构是这样的:

// 价格队列的简化设计
struct PriceLevel {
    double price;           // 价格
    OrderList orders;       // 该价格下的订单列表(FIFO)
    PriceLevel* next;       // 指向下一个价格节点
    PriceLevel* prev;       // 指向上一个价格节点
};

// 订单簿
struct OrderBook {
    PriceLevel* bidHead;    // 买单价格链表的头(最高价)
    PriceLevel* askHead;    // 卖单价格链表的头(最低价)
    std::map<double, PriceLevel*> priceMap;  // 价格到节点的映射
};

这里我用了一个双向链表来维护价格层级。为什么不用数组?因为价格是离散的,而且会动态增减。双向链表插入删除都是O(1),配合map做价格查找,完美。

避坑指南:我曾经在价格队列里直接用std::list,结果每次查找价格都要遍历。后来改成「map + 链表」的组合,性能提升了10倍。记住:查找用map,遍历用链表

2.3 时间优先队列

时间优先,说白了就是「先来后到」。同一价格下,先到的订单先成交。

实现上,每个价格队列内部就是一个FIFO队列。但这里有个细节:订单的到达顺序必须全局唯一

我见过有人用订单ID自增来做时间排序,这其实没问题。但要注意:如果系统是分布式的,多个节点同时生成订单,那ID的全局递增就需要额外处理。

一个更简单的做法:用入队时间戳 + 自增序列号。时间戳精确到微秒,同一微秒内用序列号区分。

// 时间优先队列的实现
struct Order {
    uint64_t orderId;       // 订单ID
    double price;           // 价格
    uint64_t quantity;      // 数量
    uint64_t timestamp;     // 入队时间戳(微秒)
    uint32_t seqNo;         // 同一微秒内的序列号
    Order* next;            // 链表指针
};

// 价格队列
struct PriceQueue {
    double price;
    Order* head;            // 队首(最早到达的订单)
    Order* tail;            // 队尾(最晚到达的订单)
    uint32_t orderCount;    // 订单数量
};

这里我用了链表来实现FIFO。为什么不用std::queue?因为撮合时经常需要从中间删除订单(比如部分成交后撤单),链表操作更灵活。

2.4 整体结构图

说了这么多,咱们画张图看看整体结构。下面这张SVG图展示了订单簿的核心数据流:

订单簿核心数据结构 买单队列 (Bid) 卖单队列 (Ask) 价格: 100.50 (最高买价) 价格: 100.40 价格: 100.30 价格: 100.60 (最低卖价) 价格: 100.70 价格: 100.80 时间优先队列 (FIFO) 订单A 订单B 订单C 时间优先队列 (FIFO) 订单X 订单Y 订单Z 撮合引擎 价格优先:买单高价优先,卖单低价优先 时间优先:同一价格下,先到先成交

2.5 实战中的设计要点

光有理论不够,咱们聊聊实战中容易踩的坑。

设计要点 说明 我的建议
内存分配 订单对象频繁创建销毁 用对象池,避免new/delete
并发访问 多线程读写订单簿 读写锁 + 无锁队列
价格精度 浮点数比较有误差 用整数表示价格(如1分=1单位)
队列长度 某个价格堆积大量订单 限制单价格最大订单数

警告:千万不要用浮点数直接比较价格!我见过一个线上事故,因为浮点数精度问题,两个明明相等的价格被判定为不同,导致撮合逻辑出错。解决方案:所有价格乘以10000转成整数

2.6 性能优化小技巧

最后分享几个我常用的优化技巧:

  • 预分配内存:启动时分配好订单对象池,运行时直接复用
  • 缓存友好:把订单对象设计成连续内存布局,减少CPU缓存miss
  • 批量处理:同一价格的订单批量插入,减少锁竞争
  • 懒删除:撤单时先标记,等撮合到该订单时再真正删除

嗯,这些技巧说起来简单,但真正落地的时候,每个细节都可能成为瓶颈。我当年优化一个撮合引擎,光是订单簿的内存布局就调了三个版本,才把延迟从10微秒降到2微秒。

说白了,数据结构设计没有银弹。你得根据业务场景、硬件环境、性能要求来权衡。但核心原则不变:价格优先 + 时间优先,这是所有撮合引擎的基石。


专注资料整理