1. 引言:为什么撮合引擎需要跳表
在 Web3 现货交易所里,撮合引擎是整个系统的性能心脏。它需要在毫秒甚至微秒级别完成「价格优先、时间优先」的订单匹配,同时支撑每秒数万到数十万笔的订单插入、撤销与查询。
传统实现里,订单簿(Order Book)通常有两种组织方式:
- 数组 / 有序列表:查询快,但插入和删除是 O(n),在深度大的交易对(如 BTC/USDT 动辄上万档)上会迅速成为瓶颈。
- 红黑树 / 平衡二叉树:插入、删除、查询都是 O(log n),但节点分散、指针跳转多,缓存不友好,且并发实现复杂。
跳表(Skip List) 恰好在这两者之间取得了很好的平衡:它用多层链表实现近似 O(log n) 的查找、插入、删除,结构简单、无旋转、易并发、缓存局部性优于树,因此被大量撮合引擎与内存数据库采用(如 Redis 的有序集合底层就是跳表)。
本文从撮合引擎的实际需求出发,讲清楚跳表的原理、在现货订单簿中的落地方式,以及工程实现中的关键细节。
2. 现货订单簿的数据模型
在讲跳表之前,先明确订单簿要存什么。
一个现货交易对(如 BTC/USDT)的订单簿由两侧组成:
- 买盘(Bids):按价格从高到低排序,价格越高越优先成交。
- 卖盘(Asks):按价格从低到高排序,价格越低越优先成交。
每一侧本质上是一个「价格档位」的有序集合,每个价格档位下挂着一个 FIFO 的订单队列(同一价格按时间优先)。
Bids(价格降序) Asks(价格升序)
10050 -> [订单A, 订单B] 10060 -> [订单C]
10040 -> [订单D] 10070 -> [订单E, 订单F]
10030 -> [订单G] 10080 -> [订单H]
撮合时,只要「最高买价 >= 最低卖价」,就说明存在可成交机会,引擎从两侧取出队首订单进行撮合。
因此订单簿的核心操作可以归纳为:
| 操作 | 说明 | 频率 |
|---|---|---|
| 插入订单 | 新订单进入对应价格档位 | 极高 |
| 删除订单 | 撤单或成交后移除 | 极高 |
| 查询最优价 | 取买一 / 卖一 | 极高 |
| 按价格区间遍历 | 深度行情推送、市价单扫单 | 高 |
| 按订单 ID 定位 | 撤单时找到订单所在档位 | 高 |
这些操作正是跳表擅长的场景。
3. 跳表的核心原理
3.1 从链表到多层索引
普通有序链表查找需要从头遍历,复杂度 O(n)。跳表的思路是:在链表之上建立多层「快速通道」,上层节点稀疏、下层节点稠密,查找时从最高层开始,能往右就往右,不能往右就下降一层。
Level 3: 1 -------------------------> 9
Level 2: 1 --------> 5 -------------> 9
Level 1: 1 --> 3 --> 5 --> 7 -------> 9
Level 0: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9
查找 7 的过程:
- 从 Level 3 头节点出发,下一个是 9,比 7 大,下降。
- Level 2 到 5,5 < 7,继续往右到 9,9 > 7,下降。
- Level 1 到 5,往右到 7,命中。
整个过程跳过了大量节点,平均查找长度为 O(log n)。
3.2 层高如何决定
每个节点在插入时随机决定自己的层高,通常采用几何分布:
P(level >= k) = p^k
常见取 p = 0.25 或 p = 0.5。p = 0.25 时,每 4 个节点期望有 1 个进入上一层,空间开销约为 1/(1-p) = 1.33 倍,非常划算。
随机化的好处是不需要任何重平衡操作,插入删除只改指针,天然适合高并发与低延迟场景。
3.3 复杂度分析
| 操作 | 平均复杂度 | 最坏复杂度 |
|---|---|---|
| 查找 | O(log n) | O(n) |
| 插入 | O(log n) | O(n) |
| 删除 | O(log n) | O(n) |
最坏情况概率极低(随机层高退化),工程上可以认为稳定在 O(log n)。
4. 用跳表实现订单簿
4.1 价格档位作为跳表节点
订单簿的每一侧用一个跳表维护价格档位,key 为价格,value 为该价格下的订单队列。
- 买盘跳表按价格降序比较(价格高者在前)。
- 卖盘跳表按价格升序比较(价格低者在前)。
这样「买一 / 卖一」就是跳表的第一个有效节点,取最优价是 O(1)。
4.2 插入订单流程
1. 校验订单(价格、数量、交易对)
2. 在对应侧跳表中查找价格档位
- 存在:追加到该档位的订单队列尾部(时间优先)
- 不存在:新建档位节点,插入跳表
3. 触发撮合检查:比较买一与卖一
4. 若可成交,循环撮合直到价差恢复或订单耗尽
4.3 撤单流程
撤单需要「按订单 ID 快速定位」。常见做法是维护一个 orderId -> (side, price, orderNode) 的哈希索引:
1. 通过哈希表 O(1) 找到订单所在档位
2. 从档位的订单队列中移除该订单(双向链表 O(1))
3. 若档位订单队列为空,从跳表中删除该价格节点
这里跳表的删除同样是 O(log n),且只需修改前后指针。
4.4 撮合循环
while (bestBid.price >= bestAsk.price) {
bidOrder = bestBid.queue.head
askOrder = bestAsk.queue.head
matchedQty = min(bidOrder.qty, askOrder.qty)
// 生成成交记录,更新双方余额
// 扣减数量,数量为 0 的一方出队
// 档位为空则从跳表删除
}
由于最优价始终在跳表头部,撮合循环的每次迭代都是 O(1) 取档位 + O(log n) 删除空档位,整体效率很高。
5. 代码实现示例
下面用 Go 给出一个简化但可运行的跳表实现,聚焦订单簿需要的「有序插入、查找、删除、取首节点」能力。
package skiplist
import (
"math/rand"
)
const maxLevel = 32
const p = 0.25
type node struct {

本文深入解析跳表在Web3现货撮合引擎订单簿中的核心应用。跳表以O(log n)复杂度支持高效的价格档位有序插入、删除与查询,兼顾缓存友好性与并发简易性,优于红黑树和普通链表。订单簿将买盘/卖盘分别建模为降序/升序跳表,价格为键,订单队列为值;辅以哈希索引实现按ID撤单。工程实践强调定点整数定价、单线程撮合、空档位清理及内存优化。

574

被折叠的 条评论
为什么被折叠?



