撮合引擎把买价和卖价配到一起,是所有交易系统的核心。它有两种截然不同的存法:状态放数据库,或者状态放内存。前者是绝大多数传统柜台的选择,后者是极速柜台的立身之本。差距有多大、代价是什么,这篇用数字说清楚。
数据库撮合:每一笔都是一个事务
传统做法里,订单簿就是一张表(或者一组表):档位表、委托队列表、冻结余额表。一笔委托进来:
BEGIN开启事务;- 锁定订单簿相关行(行锁,防止并发改同一档位);
- 插入委托、撮合成交、更新档位总量、解冻再冻结余额——若干条 DML;
COMMIT,redo 日志落盘(fsync,机械盘上约 1~10ms)。
哪怕全换 SSD,一次事务提交的延迟也在 0.5~2ms 量级。撮合循环里一次 INSERT 加若干 UPDATE,单笔委托的处理成本按毫秒计。更致命的是并发模型:数据库用锁保证隔离,但撮合的正确性要求"同一档位的撮合严格串行"——数据库的行锁恰好把这个串行点放在了磁盘同步这个最慢的操作上。两个线程竞争同一行的锁,等的时间不是内存纳秒,是日志落盘的毫秒。
粗算一下容量。设单笔事件(委托/撤单/成交)平均事务成本 $L_{db} \approx 2\,\text{ms}$,单库连接串行吞吐就是
$$ \mu_{db} = \frac{1}{L_{db}} \approx 500 \ \text{笔/秒} $$
想往上堆只能开并行连接,但锁竞争让加速比远小于线性——Amdahl 定律在这里的现实版:串行化点是落盘,加多少连接都在等同一个 fsync。
内存撮合:状态在 RAM,磁盘只记日志
极速柜台的做法是把订单簿整个搬进内存:红黑树存档位,每档一个侵入式队列存委托,全部指针操作,单笔事件处理在亚微秒到几微秒。数据库从热路径上彻底消失。
持久化问题怎么解?三件套:
- 输入日志(input log):撮合线程消费的所有输入按序追加写日志,WAL 思路。这一步可以异步批量刷盘,平均到每笔的 IO 成本降到微秒级;
- 定期快照:订单簿状态每 N 秒序列化一份快照落盘;
- 回放恢复:进程重启 = 加载最近快照 + 重放之后的输入日志,重建完全一致的状态。
关键洞察:撮合是确定性函数。同一个输入序列,无论从快照恢复还是从头重放,产生的订单簿状态逐字节相同。所以"内存状态"并不脆弱——它只是把数据库的持久化职责拆成了"日志 + 快照"两个更便宜的部件。
崩溃窗口里的尾巴(日志还没刷盘的部分)怎么办?这正是回报篇的重复语义发光的地方:重启后向交易所查询在途委托(断线重连标准报文),用权威方数据补齐,重复的部分靠幂等键消化。
代价与边界
内存撮合不是免费的午餐,清单要认全:
- 状态大小受限于内存:单市场全量订单簿是 GB 量级,没问题;但如果要做跨市场的全量组合风控,内存账单要重新算;
- 水平扩展性差:内存状态是单机的,分片靠"一个合约/一个账户固定落在某分区",天然抗拒弹性扩缩;
- 正确性全靠工程纪律:没有数据库的约束和隔离级别兜底,指针错误、内存踩踏直接是错账。这就是为什么内存撮合引擎普遍配确定性回放测试——拿生产日志回放,比对状态哈希,任何一次不一致都是 P0。
队列论视角:为什么不能"慢一点点"
单线程撮合循环是个经典排队系统:到达率 $\lambda$,服务率 $\mu$,队列长度
$$ L_q = \frac{\lambda^2}{\mu(\mu - \lambda)}, \qquad W = \frac{1}{\mu - \lambda} $$
注意 $W$ 在 $\lambda \to \mu$ 时发散。撮合延迟的均值看着还行,尾部在接近饱和时爆炸——行情火爆时委托量三倍于平日,$\lambda$ 贴着 $\mu$ 走,排队的委托每多排 1ms,客户的撤单率就涨一截,撤单又增加队列负担(撤单篇说过撤单与委托等量排队)。延迟和负载在这里是正反馈。数据库撮合的 $\mu$ 只有几百,离真实峰值太近;内存撮合把 $\mu$ 抬高两个数量级,本质是把系统推到反馈环的左侧安全区。
选型结论不复杂:面向零售、吞吐量每秒几百笔的传统柜台,数据库撮合的运维友好性(SQL 可查、约束兜底、随扩随缩)值回延迟成本;面向量化客户的极速柜台,客户付的钱就是买 $\mu$ 抬高两个数量级之后的排队余量。两条路线的分野不在技术品味,在你的客户愿意为多少微秒付费。