数据结构设计:订单簿的核心选择

做撮合引擎,说白了就是在跟订单簿打交道。

订单簿就是那个记录所有买单卖价的账本。你想想看,每秒可能有几千笔订单砸进来,每一笔都要找到合适的对手方成交。数据结构选错了,系统就卡死了。

我当年第一次做撮合引擎时,就踩过这个坑。当时图省事,直接用了个数组加链表,结果压测到500笔/秒就崩了。后来老老实实研究数据结构,才明白这里面门道有多深。

订单簿的核心需求

我们先理清楚,订单簿到底要干什么:

  • 快速插入:新订单进来,要立刻放到正确的位置
  • 快速查询:找到最优的买价和卖价
  • 快速删除:订单撤销或成交后,要能迅速移除
  • 范围查询:比如查询某个价格区间内的所有订单

嗯,这几个需求,不同的数据结构表现天差地别。

三种主流数据结构对比

数据结构 插入 查询最优 删除 范围查询
红黑树 O(log n) O(log n) O(log n) O(log n + k)
跳表 O(log n) O(log n) O(log n) O(log n + k)
优先队列 O(log n) O(1) O(n) 不支持

看到没?优先队列查最优价最快,但删除和范围查询就拉胯了。红黑树和跳表各方面都比较均衡。

我的建议:生产环境用红黑树或跳表。优先队列只适合做演示或极简场景。

红黑树:最稳妥的选择

红黑树是平衡二叉搜索树的一种。它保证了最坏情况下操作也是O(log n)。

我习惯用红黑树做价格维度的索引。每个价格节点下面挂一个订单链表,这样价格排序和订单管理就分开了。

// 伪代码示例:红黑树 + 价格队列
class PriceNode {
    double price;
    OrderList orders;  // 该价格下的订单链表
    Color color;       // 红黑树节点颜色
    PriceNode left, right, parent;
}

class OrderBook {
    RedBlackTree<PriceNode> buyTree;   // 买单红黑树
    RedBlackTree<PriceNode> sellTree;  // 卖单红黑树
}

这里有个细节:红黑树的旋转操作在并发场景下要小心。我遇到过因为旋转时没加锁,导致两个线程同时修改树结构,直接挂了。后来改成了读写锁分离才解决。

跳表:并发友好的替代方案

跳表本质上是个多层链表。它用概率来保证平衡,而不是像红黑树那样用旋转。

为什么说跳表并发友好?因为它的修改操作是局部性的,只需要锁住相邻的几个节点就行。红黑树的旋转可能影响整棵树。

// 跳表节点结构
class SkipListNode {
    double price;
    OrderList orders;
    SkipListNode[] forward;  // 不同层的指针
    int level;               // 节点层数
}

经验之谈:如果你的撮合引擎是单线程的,红黑树完全够用。如果是多线程的,跳表可能更省心。

价格队列与时间优先队列

光有价格排序还不够。同一个价格下,订单还要按时间排序。这就是价格队列和时间优先队列的组合。

价格队列的结构很简单:

class OrderList {
    Order head;  // 链表头
    Order tail;  // 链表尾
    int count;   // 订单数量
}

class Order {
    long orderId;
    long timestamp;
    long quantity;
    Order prev, next;
}

时间优先队列,说白了就是先进先出。同一个价格,先来的订单先成交。这个用链表实现最合适,插入到尾部O(1),从头部取出也O(1)。

注意:时间戳的精度问题。我曾经遇到过两个订单在同一微秒到达,结果时间戳一样,导致排序出问题。后来我改用全局自增序列号来保证时间顺序。

整体架构图

下面这张图展示了订单簿的核心结构:

订单簿数据结构架构 买单红黑树 (Buy Tree) 卖单红黑树 (Sell Tree) 价格: 100.5 价格: 100.3 价格: 100.1 价格: 100.6 价格: 100.8 价格: 101.0 价格队列 (Price Queue) 价格队列 (Price Queue) 订单A (t1) 订单B (t2) 订单C (t3) 订单X (t1) 订单Y (t2) 订单Z (t3) 时间优先:t1 < t2 < t3,先到先成交 最优买价:100.1(红黑树最左节点) 最优卖价:100.6(红黑树最左节点) 每个价格节点下挂一个订单链表,按时间戳排序 插入:O(log n) | 查询最优:O(log n) | 删除:O(log n)

实际选型建议

说了这么多,到底怎么选?我给出几个场景:

  • 单线程撮合引擎:红黑树 + 链表。简单可靠,性能足够。
  • 多线程撮合引擎:跳表 + 链表。并发性能更好。
  • 极高频交易:可以考虑用数组 + 价格分桶。牺牲灵活性换速度。
  • 学习演示:优先队列 + 链表。代码简单,容易理解。

一个小技巧:不管用哪种数据结构,记得把价格和订单分开存储。价格用树或跳表索引,订单用链表管理。这样修改订单时不用动树结构,效率高很多。

嗯,数据结构这块就聊这么多。记住一句话:没有最好的数据结构,只有最适合你场景的。选之前,先想清楚你的瓶颈在哪里。

专注资料整理