订单簿结构深度拆解:买盘队列与卖盘队列的组织方式
大家好,我是老张。今天咱们来聊聊订单簿的核心——买盘和卖盘队列是怎么组织的。说白了,这就是交易所撮合引擎的「心脏」。
我记得刚入行那会儿,总觉得订单簿就是个简单的价格列表。后来自己动手写撮合引擎,才发现里面的门道比想象中深得多。你想想看,一个每秒处理几万笔订单的系统,队列组织不好,分分钟就崩了。
买盘队列与卖盘队列的基本结构
先看个最直观的结构图:
看到这个图了吗?买盘队列是价格从高到低排列,卖盘队列是价格从低到高排列。为什么这么设计?
因为买方当然希望用更低的价格买入,但系统要优先满足出价最高的买家。同理,卖方希望卖得越贵越好,但系统优先处理要价最低的卖家。这就是价格优先原则的直观体现。
核心要点:买盘队列按价格降序排列,卖盘队列按价格升序排列。最高买价和最低卖价就是「最佳买卖价」,它们之间的差值叫「价差」。
价格优先原则的底层实现
价格优先,说白了就是「出价高的买家优先成交,要价低的卖家优先成交」。这个逻辑听着简单,但实现起来有几个坑。
我曾经在一个项目中,直接用数组来维护订单队列。每次插入新订单都要遍历整个数组找位置,性能惨不忍睹。后来改用跳表(Skip List),插入和查找都是 O(log n),这才解决问题。
来看看价格优先的具体实现逻辑:
// 价格优先的核心比较逻辑
// 买盘:价格高的优先
function compareBid(priceA, priceB) {
if (priceA > priceB) return -1; // A排在前面
if (priceA < priceB) return 1; // B排在前面
return 0; // 价格相同,交给时间优先
}
// 卖盘:价格低的优先
function compareAsk(priceA, priceB) {
if (priceA < priceB) return -1; // A排在前面
if (priceA > priceB) return 1; // B排在前面
return 0; // 价格相同,交给时间优先
}
你可能会问:为什么买盘和卖盘的比较逻辑是反的?嗯,这个问题问得好。因为买盘队列中,价格最高的在最前面,所以比较函数要返回「价格高的排前面」。卖盘则相反。
时间优先原则的底层实现
价格相同的时候怎么办?这时候就看谁先到。时间优先原则就是「先来后到」。
我建议在订单数据结构里加一个时间戳字段。这个时间戳必须是单调递增的,不能直接用系统时间——因为系统时间可能回拨,或者多个订单在同一毫秒到达。
// 订单数据结构
class Order {
constructor(id, price, quantity, side) {
this.id = id; // 订单ID
this.price = price; // 价格
this.quantity = quantity; // 数量
this.side = side; // 'buy' 或 'sell'
this.timestamp = Date.now(); // 时间戳
this.sequence = nextSequence++; // 全局递增序号
}
}
// 时间优先比较
function compareByTime(orderA, orderB) {
if (orderA.sequence < orderB.sequence) return -1;
if (orderA.sequence > orderB.sequence) return 1;
return 0;
}
实战技巧:我个人习惯用全局递增序号代替时间戳来做时间优先比较。因为时间戳在毫秒级别可能重复,而序号是严格递增的。另外,序号还能用来检测订单的先后顺序,排查问题的时候特别好用。
价格优先 + 时间优先的联合排序
把两个原则结合起来,就是完整的订单队列排序逻辑。先按价格排,价格相同再按时间排。
// 买盘队列的完整排序
function compareBidOrders(orderA, orderB) {
// 第一步:价格优先
if (orderA.price !== orderB.price) {
return orderA.price > orderB.price ? -1 : 1;
}
// 第二步:时间优先
return orderA.sequence - orderB.sequence;
}
// 卖盘队列的完整排序
function compareAskOrders(orderA, orderB) {
// 第一步:价格优先
if (orderA.price !== orderB.price) {
return orderA.price < orderB.price ? -1 : 1;
}
// 第二步:时间优先
return orderA.sequence - orderB.sequence;
}
这段代码看着简单,但它是整个撮合引擎的基石。我见过不少新手把比较逻辑写反了,结果撮合出来的成交价乱七八糟。
避坑指南:我曾经在一个项目中,把买盘队列的排序写成了「价格低的优先」。结果上线后,所有买单都跟最低价的卖单成交,市场瞬间被打穿。幸好是在测试环境发现的,不然就出大事了。所以排序逻辑一定要反复检查。
队列的数据结构选择
订单队列用什么数据结构?这是个好问题。不同的场景有不同的选择:
| 数据结构 | 插入复杂度 | 删除复杂度 | 适用场景 |
|---|---|---|---|
| 有序数组 | O(n) | O(n) | 小规模订单簿 |
| 跳表 (Skip List) | O(log n) | O(log n) | 中等规模,读写均衡 |
| 红黑树 | O(log n) | O(log n) | 高性能场景 |
| 堆 (Heap) | O(log n) | O(log n) | 只取最优价格 |
我个人比较推荐用跳表。为什么?因为红黑树实现起来太复杂,容易出bug。跳表代码简单,性能也够用。我在一个日交易量过亿的交易所里就用跳表,跑了两年没出过问题。
实际项目中的队列管理
光有排序还不够,还得考虑订单的增删改查。我总结了几条实战经验:
- 批量插入优化:如果同时涌入大量订单,先排序再批量插入,比逐个插入快很多。
- 价格档位聚合:相同价格的订单可以合并成一个节点,里面再维护一个时间有序的子队列。这样能减少树的高度。
- 缓存最佳价格:把最佳买价和最佳卖价缓存起来,撮合的时候直接取,不用每次都查树。
- 内存对齐:订单对象的内存布局要注意对齐,能提升缓存命中率。
总结一下:订单簿的核心就是「价格优先 + 时间优先」的排序逻辑。买盘队列价格从高到低,卖盘队列价格从低到高。实现时用全局递增序号保证时间顺序,用跳表或红黑树保证性能。记住,排序逻辑写反了会出大问题。
好了,这一章就聊到这儿。下一章咱们聊聊撮合引擎的核心循环——怎么把买单和卖单匹配起来。到时候我会分享一个我踩过的坑,保证让你印象深刻。