Day 99
Day 99:撮合引擎设计 — 从LMAX到链上撮合
价格-时间优先vs按比例分配、LMAX Disruptor单线程600万单/秒、事件溯源+WAL、红黑树数据结构、链上撮合三种方案(全链上/混合/ZK证明)
2026-04-16
交易撮合引擎LMAX订单簿红黑树链上撮合Day99
核心概念
什么是撮合引擎?
一句话定义:撮合引擎(Matching Engine)是交易所的心脏,负责将买卖双方的订单按照既定规则进行匹配成交。它是所有交易系统中延迟最敏感、吞吐量要求最高的组件。
类比理解:撮合引擎就像一个超级高效的"拍卖师"——在传统拍卖中,拍卖师需要记住所有出价,比较金额,决定成交。撮合引擎做同样的事情,只不过它每秒钟要处理数百万次"拍卖"。
撮合引擎在交易系统中的位置
交易系统完整链路中的撮合引擎定位:
═══════════════════════════════════════
用户下单 → [API Gateway]
↓
[风控预检] ← 资金/仓位/限额检查
↓
[订单管理(OMS)] ← 订单生命周期管理
↓
┌────────────────────┐
│ ★ 撮合引擎 ★ │ ← 核心!决定谁和谁成交
│ Matching Engine │
│ │
│ 买单簿 ←→ 卖单簿 │
└────────────────────┘
↓
[成交回报]
/ \
[清算系统] [市场数据]
↓ ↓
[结算系统] [行情推送]
↓
[资产托管]
知识点详解
知识点 1:撮合算法 — 价格-时间优先 vs 按比例分配
价格-时间优先(Price-Time Priority / FIFO)
这是最常见的撮合规则,也称为 FIFO(First In, First Out)。
价格-时间优先规则:
═══════════════════════════════════════
规则 1:价格优先
买单:出价越高,越优先成交
卖单:出价越低,越优先成交
规则 2:时间优先(同价格下)
先到的订单,优先成交
示例场景:
═══════════════════════════════════════
当前卖单簿(按价格升序):
卖出 10 BTC @ $100.5 [时间: 10:00:01]
卖出 5 BTC @ $100.5 [时间: 10:00:03]
卖出 20 BTC @ $101.0 [时间: 10:00:02]
新来一个市价买单:买 12 BTC
撮合过程:
Step 1:先成交 $100.5 的卖单(价格最优)
Step 2:$100.5 有两个卖单,先成交 10:00:01 的(时间优先)
→ 成交 10 BTC @ $100.5
Step 3:还差 2 BTC,成交 10:00:03 的
→ 成交 2 BTC @ $100.5
结果:
买方:12 BTC,均价 $100.5
卖方1:全部成交 10 BTC
卖方2:部分成交 2 BTC(剩余 3 BTC 挂单)
按比例分配(Pro-Rata)
按比例分配规则:
═══════════════════════════════════════
同一价格水平的订单,按各自数量占比分配成交量
示例:
卖单A:10 BTC @ $100.5(占 66.7%)
卖单B:5 BTC @ $100.5(占 33.3%)
买单:6 BTC @ $100.5
按比例分配:
卖单A 成交:6 × 66.7% = 4 BTC
卖单B 成交:6 × 33.3% = 2 BTC
两种算法对比
| 维度 | 价格-时间优先 (FIFO) | 按比例分配 (Pro-Rata) |
|---|---|---|
| 公平性 | 先到先得 | 按比例分配 |
| 适用场景 | 股票、加密货币 | 期权、大宗商品期货 |
| 对速度要求 | 高(抢先排队优势) | 低(排队位置不重要) |
| 流动性激励 | 鼓励快速下单 | 鼓励大额挂单 |
| 代表交易所 | NYSE, Binance, CME(多数) | CME(部分品种), CBOE |
| 优点 | 简单、可预测、鼓励报价 | 鼓励大单、减少速度竞争 |
| 缺点 | HFT的速度军备竞赛 | 大户优势、小单不利 |
知识点 2:LMAX Disruptor — 单线程600万单/秒的秘密
LMAX交易所的架构革命
LMAX Exchange 是一家外汇交易所,他们用一个单线程撮合引擎达到了 600 万单/秒的处理速度。这颠覆了"多线程=高性能"的传统认知。
LMAX 核心设计理念:
═══════════════════════════════════════
传统思路:多线程 + 锁 → 复杂 + 竞争 + 上下文切换开销
LMAX 思路:单线程 + 无锁 + 机械同情(Mechanical Sympathy)
关键洞察:
├── CPU 缓存友好 > 多线程
│ └── L1 cache 读取:~0.5ns
│ └── 线程上下文切换:~10,000ns(2万倍!)
│
├── 避免锁竞争
│ └── 有锁:CAS + 内存屏障 + 可能的线程挂起
│ └── 无锁:单线程天然无竞争
│
└── 预分配 + 无GC
└── Ring Buffer 预分配内存
└── 避免 GC Stop-The-World 暂停
Disruptor 环形缓冲区
Disruptor Ring Buffer 核心结构:
═══════════════════════════════════════
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ ← 预分配的固定大小数组
└───┴───┴───┴───┴───┴───┴───┴───┘
↑ ↑
Consumer Producer
(读指针) (写指针)
特点:
├── 固定大小(通常是 2^n,如 1024, 4096)
├── 预分配所有 Entry 对象(无 new/delete)
├── 生产者:原子递增 sequence → 写入对应槽位
├── 消费者:检查 sequence → 读取对应槽位
├── 无锁:CAS 操作代替互斥锁
└── 缓存行填充:防止 false sharing
vs 传统队列:
ArrayBlockingQueue:锁竞争 + GC 压力
LinkedBlockingQueue:缓存不友好 + GC 压力
Disruptor:无锁 + 无GC + 缓存友好 = 10-100x 性能提升
LMAX 完整架构
LMAX 撮合引擎完整架构:
═══════════════════════════════════════
┌─────────────────────────────────────┐
│ Input Disruptor │ ← 接收订单
│ [Ring Buffer: 订单事件队列] │
└──────────────┬──────────────────────┘
↓
┌─────────────────────────────────────┐
│ Business Logic Processor │ ← 单线程!
│ ┌──────────┐ ┌──────────────┐ │
│ │ 撮合引擎 │ │ 风控检查 │ │
│ │ (内存中) │ │ (内存中) │ │
│ └──────────┘ └──────────────┘ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ 订单簿 │ │ 仓位管理 │ │
│ │ (内存中) │ │ (内存中) │ │
│ └──────────┘ └──────────────┘ │
└──────────────┬──────────────────────┘
↓
┌─────────────────────────────────────┐
│ Output Disruptor │ ← 输出结果
│ [Ring Buffer: 成交/行情事件队列] │
└──────────────┬──────────────────────┘
↓
┌────────┴────────┐
[行情推送] [持久化]
(多线程扇出) (异步写入Journal)
关键数据:
├── 延迟:< 1ms(99th percentile)
├── 吞吐:600万事件/秒
├── 全部状态在内存中(~50GB)
├── Journal 用于灾难恢复(事件溯源)
└── 无数据库写入在关键路径上!
知识点 3:事件溯源(Event Sourcing)+ WAL
为什么撮合引擎需要事件溯源?
撮合引擎的持久化挑战:
═══════════════════════════════════════
问题:
├── 每秒百万级事件
├── 延迟要求 < 1ms
├── 不能用传统数据库(太慢)
└── 但必须保证数据不丢失(金融系统!)
解决方案:事件溯源 + WAL(Write-Ahead Log)
═══════════════════════════════════════
1. 所有状态变更记录为不可变事件
2. 先写 WAL(顺序写入,极快)
3. 内存状态从事件序列重建
4. 定期快照(Snapshot)加速恢复
事件流示例:
Event #1001: NewOrder(id=A, side=Buy, price=100, qty=10)
Event #1002: NewOrder(id=B, side=Sell, price=100, qty=5)
Event #1003: Trade(buyer=A, seller=B, price=100, qty=5)
Event #1004: OrderPartialFill(id=A, filled=5, remaining=5)
Event #1005: OrderFullFill(id=B, filled=5)
恢复过程:
1. 加载最近的 Snapshot(如 Event #900 时的状态)
2. 重放 Event #901 到 #1005
3. 完全恢复内存状态
4. 总恢复时间:< 10秒(百万级事件)
WAL 的写入优化
WAL 性能优化策略:
═══════════════════════════════════════
1. 顺序写入(Sequential Write)
└── HDD 顺序写:~100 MB/s
└── SSD 顺序写:~500 MB/s - 3 GB/s
└── 对比随机写:慢 100-1000x
2. 批量刷盘(Group Commit)
└── 不是每个事件都 fsync
└── 攒一批(如 1ms 内的事件)一起刷
└── 牺牲极少耐久性,换大幅吞吐提升
3. 内存映射文件(mmap)
└── 操作系统管理页面缓存
└── 减少用户态/内核态切换
4. 双缓冲(Double Buffering)
└── Buffer A 接收新事件
└── Buffer B 同时刷盘
└── 写满后交换
知识点 4:红黑树 — 订单簿的数据结构
为什么选择红黑树?
订单簿的数据结构选型:
═══════════════════════════════════════
需求:
├── 快速插入新订单(按价格排序)
├── 快速删除(取消订单)
├── 快速查找最优价格(最高买价/最低卖价)
├── 按价格遍历(生成市场深度)
└── 高频操作(每秒百万次)
候选方案对比:
┌──────────────┬──────────┬──────────┬──────────┬──────────┐
│ 数据结构 │ 插入 │ 删除 │ 查找极值 │ 遍历 │
├──────────────┼──────────┼──────────┼──────────┼──────────┤
│ 排序数组 │ O(n) │ O(n) │ O(1) │ O(k) │
│ 哈希表 │ O(1) │ O(1) │ O(n) │ O(n) │
│ 红黑树 │ O(log n) │ O(log n) │ O(log n) │ O(k) │
│ 跳表 │ O(log n) │ O(log n) │ O(1) │ O(k) │
│ B+树 │ O(log n) │ O(log n) │ O(log n) │ O(k) │
└──────────────┴──────────┴──────────┴──────────┴──────────┘
结论:红黑树和跳表是主流选择
├── 红黑树:Java TreeMap(CME, LMAX 常用)
├── 跳表:C++ 实现(部分高频交易系统)
└── 实际中,价格档位数 n < 10000,O(log n) ≈ 13次比较
订单簿的内存结构
订单簿内存布局(红黑树 + 双向链表):
═══════════════════════════════════════
卖单簿(Ask Book)- 红黑树,按价格升序
┌─────────────────────────────────────┐
│ Price: $101.0 │
│ Orders: [Sell 5@101.0] → [Sell 3@101.0] → null
│ (FIFO链表) │
├─────────────────────────────────────┤
│ Price: $100.5 │ ← Best Ask
│ Orders: [Sell 10@100.5] → [Sell 7@100.5] → null
└─────────────────────────────────────┘
↕ Spread = $0.2
┌─────────────────────────────────────┐
│ Price: $100.3 │ ← Best Bid
│ Orders: [Buy 8@100.3] → null │
├─────────────────────────────────────┤
│ Price: $100.0 │
│ Orders: [Buy 15@100.0] → [Buy 20@100.0] → null
└─────────────────────────────────────┘
买单簿(Bid Book)- 红黑树,按价格降序
核心操作复杂度:
├── 插入限价单:O(log P) P=价格档位数
├── 取消订单:O(1)(通过订单ID直接定位,HashMap辅助)
├── 撮合:O(1) 获取 Best Bid/Ask + O(1) 移除队首订单
└── 市场深度查询:O(k) k=需要的档位数
伪代码实现
// 撮合引擎核心伪代码
class MatchingEngine {
bidBook: RedBlackTree<Price, OrderQueue> // 买单簿
askBook: RedBlackTree<Price, OrderQueue> // 卖单簿
orderMap: HashMap<OrderId, Order> // 快速查找
function processLimitOrder(order):
if order.side == BUY:
// 尝试与卖单簿撮合
while askBook.bestPrice() <= order.price AND order.remaining > 0:
bestAsk = askBook.bestOrder()
tradeQty = min(order.remaining, bestAsk.remaining)
executeTrade(order, bestAsk, tradeQty, bestAsk.price)
if bestAsk.remaining == 0:
askBook.removeOrder(bestAsk)
// 未完全成交,挂入买单簿
if order.remaining > 0:
bidBook.addOrder(order)
orderMap.put(order.id, order)
// SELL 类似,镜像操作
function cancelOrder(orderId):
order = orderMap.get(orderId)
if order.side == BUY:
bidBook.removeOrder(order)
else:
askBook.removeOrder(order)
orderMap.remove(orderId)
}
知识点 5:链上撮合 — 三种方案对比
全链上撮合
方案1:全链上订单簿(如 Serum/Phoenix on Solana)
═══════════════════════════════════════
用户 → [链上合约:订单簿 + 撮合逻辑]
│
├── 下单:发交易 → 写入链上订单簿
├── 撮合:Cranker/Keeper 触发撮合
├── 结算:合约内原子结算
└── 取消:发交易 → 从链上删除
优点:
├── 完全去中心化,无需信任
├── 可组合性(其他合约可调用)
├── 透明可验证
└── 抗审查
缺点:
├── 性能受限于区块链吞吐量
│ └── Solana:~400 TPS vs CeFi:100万+ TPS
├── Gas 成本高(每笔操作都是链上交易)
├── 延迟高(出块时间 400ms-12s)
├── 前置运行(Front-running)风险
└── 订单簿更新需要链上交易(成本)
代表项目:
├── Phoenix(Solana)
├── Serum(Solana,已关闭)
└── 早期 EtherDelta(Ethereum)
链下撮合 + 链上结算(混合模式)
方案2:链下撮合 + 链上结算(如 dYdX v3, Hyperliquid)
═══════════════════════════════════════
用户 → [链下: 中心化撮合引擎]
│
├── 下单/取消:API调用(免Gas)
├── 撮合:中心化引擎(微秒级)
└── 结算:定期批量提交链上
│
↓
[链上合约: 验证 + 结算]
├── 验证签名
├── 更新余额
└── 保管资产
优点:
├── CeFi级性能(毫秒延迟,百万TPS)
├── 下单/取消免Gas
├── 用户体验好(类似中心化交易所)
└── 资产自托管(链上合约保管)
缺点:
├── Sequencer 中心化(单点故障)
├── 可能审查交易
├── 可组合性受限
└── 需要信任运营方不作恶
代表项目:
├── Hyperliquid
├── dYdX v3(StarkEx)
└── ApeX Protocol
ZK证明撮合
方案3:ZK 证明撮合公平(如 Lighter)
═══════════════════════════════════════
用户 → [链下: 撮合引擎]
│
├── 下单/撮合:链下执行
└── 生成 ZK Proof:
"我证明撮合过程是公平的,
没有插队、没有忽略订单"
│
↓
[链上合约]
├── 验证 ZK Proof(O(1) 成本)
├── 更新状态根
└── 如果证明无效 → 拒绝
优点:
├── 接近CeFi性能
├── 数学证明公平(Sequencer不可作恶)
├── 继承L1安全性
└── 无需信任运营方
缺点:
├── ZK电路开发复杂
├── 证明生成需要算力
├── 技术成熟度较低
└── 状态最终确认有延迟(等ZK证明生成)
代表项目:
├── Lighter(ZK证明撮合公平)
├── Paradex(ZK加密订单簿)
└── 未来 dYdX v5
三种方案对比总结
| 维度 | 全链上 | 混合模式 | ZK证明 |
|---|---|---|---|
| 性能 | 低(区块链限制) | 高(CeFi级) | 高(链下执行) |
| 延迟 | 400ms-12s | <10ms | <10ms |
| 去中心化 | 最高 | 低 | 中-高 |
| 信任假设 | 无需信任 | 信任Sequencer | 信任数学 |
| Gas成本 | 高 | 低(批量结算) | 低(ZK验证) |
| 可组合性 | 最好 | 受限 | 受限 |
| 技术难度 | 中 | 低 | 极高 |
| 代表 | Phoenix | Hyperliquid | Lighter |
| 适用场景 | 低频、去中心化 | 高频、专业交易 | 高频+公平性 |
面试题解析
如何设计支持百万TPS的撮合引擎?
简短回答(30秒版本):
核心是单线程无锁设计 + 事件溯源。参考LMAX架构:用Ring Buffer接收订单,单线程业务处理器在内存中维护订单簿(红黑树),所有状态变更写WAL异步持久化。关键是消除线程竞争和GC暂停,实现机械同情(Mechanical Sympathy)。
详细回答(2分钟版本):
百万TPS撮合引擎的5层设计:
═══════════════════════════════════════
Layer 1:网络层
├── Kernel Bypass(DPDK/io_uring)
├── 协议优化:FIX/Binary Protocol
└── 连接池 + 零拷贝
Layer 2:输入队列
├── Ring Buffer(Disruptor模式)
├── 预分配内存,无GC
├── Batching:攒1ms的订单批量处理
└── 背压机制:队列满时优雅降级
Layer 3:撮合核心(单线程)
├── 内存中的订单簿(红黑树 + HashMap)
├── 所有逻辑在一个线程
├── 无锁、无IO、无系统调用
├── CPU亲和性(绑核)
└── 缓存行对齐(防止False Sharing)
Layer 4:持久化
├── 事件溯源:所有状态变更 → Event
├── WAL:先写日志再确认
├── Group Commit:批量fsync
└── 定期快照:加速恢复
Layer 5:输出
├── 成交回报 → 用户通知
├── 市场数据 → 行情系统
└── 风控事件 → 风控系统
性能关键指标:
├── 延迟:P99 < 100μs
├── 吞吐:> 1M orders/sec
├── 恢复时间:< 10s(从WAL重放)
└── 可用性:99.999%(5个9)
追问准备:
- Q:为什么选单线程而不是多线程?A:线程上下文切换 ~10μs,锁竞争更慢;单线程消除所有竞争,CPU缓存命中率更高。
- Q:如何保证高可用?A:主备热切换 + 事件溯源重放。备机实时消费事件流,主机故障时 <1s 切换。
- Q:如果单线程性能不够怎么办?A:按交易对分片(Sharding),每个分片独立的单线程引擎。BTC/USDT 和 ETH/USDT 互不影响。
产品经理视角
PM 需要理解的撮合引擎关键决策:
═══════════════════════════════════════
1. 撮合规则选择
├── FIFO:适合标准交易品种
├── Pro-Rata:适合期权等衍生品
└── 混合模式:FIFO + Pro-Rata 结合
2. 性能 vs 去中心化
├── CeFi:极致性能(LMAX模式)
├── 全链上:极致去中心化
└── 混合/ZK:平衡方案
3. 公平性设计
├── 时间优先是否公平?(HFT优势)
├── 随机延迟是否可接受?(IEX模式)
└── ZK证明是否是终极方案?
4. 从10年金融经验看
├── 传统交易所的撮合引擎已非常成熟
├── 链上撮合是全新挑战
├── 混合模式是当前最实用方案
└── ZK证明代表未来方向
明日预告
Day 100:CeFi vs DeFi 交易系统全面对比 — 从OMS到链上结算
- CeFi全链路详解(OMS→RMS→撮合→清算→结算→托管)
- DeFi如何映射传统金融的每个环节
- 8+维度深度对比分析
- 混合架构趋势与实践
- 10年金融经验在Web3的价值映射