返回 Web3 笔记
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验证)
可组合性最好受限受限
技术难度极高
代表PhoenixHyperliquidLighter
适用场景低频、去中心化高频、专业交易高频+公平性

面试题解析

如何设计支持百万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的价值映射