TL;DR
- 撮合引擎为追求低延迟,核心状态(订单簿/挂单/撮合指针)通常常驻内存;但内存易失,崩溃/断电/重启会把状态“清零”。
- 把撮合引擎建模为确定性状态机:只要有严格有序的事件流(下单/撤单/成交等),就能通过重放恢复到同一状态。
- 工程上用事件溯源 + 预写日志(WAL)保证 RPO≈0:先把事件持久化,再更新内存订单簿。
- 用快照 + 增量重放缩短 RTO:加载最近一次快照,然后从快照序列号之后开始回放事件,避免“从创世重放”。
1. 问题本质:性能要内存,可靠性要持久化
交易系统的撮合核心之所以快,是因为它把订单簿(Order Book)当成内存数据结构来操作。
但这也意味着:一旦进程崩溃、机器断电、甚至一次计划内重启,订单簿瞬间丢失。
直觉解法是“每次状态变更都同步写数据库”,但这会把瓶颈从撮合逻辑转移到磁盘 I/O、事务与锁竞争上,吞吐从百万级掉到千级并不稀奇。
所以正确方向通常是:撮合仍在内存跑,但持久化要做到不拖慢撮合。
2. 核心思路:事件溯源(Event Sourcing)= 只存“发生过什么”
把状态“还原”为事件重放的结果
- 传统方式存“当前状态”(订单表字段)。
- 事件溯源存“状态变化的事实”(下单/撤单/成交事件序列)。
- 系统当前状态 = 从某个初始状态开始按序重放事件后的结果。
这套方法成立的关键前提是确定性:同一个初始状态 + 同一条严格有序的事件流 → 必然得到同一个最终订单簿。
也因此,“事件顺序”比事件内容更敏感。
3. 关键要点/坑:WAL 与全局序列号,缺一不可
- WAL(Write-Ahead Logging):更新内存前先持久化事件。否则崩溃在最尴尬的瞬间会出现“内存改了、日志没落盘”或相反,导致恢复不一致。
- 全局严格有序:多网关/多线程接入时,必须有一个“序列器(Sequencer)”对事件分配单调递增的 Sequence ID,并以此作为重放顺序。
- 确认语义:写入日志系统时 ack 级别会直接影响 RPO 与延迟。金融系统通常宁愿选择更强的确认(更低丢失风险),再通过网络/集群调优降低额外延迟。
- 幂等与去重:现实世界会重试、会超时。事件里要有可去重的标识(例如 client_order_id / request_id),恢复/重放必须能正确处理重复输入。
4. 快照(Snapshot):让恢复时间从“重放全部”降到“重放增量”
只有事件日志会遇到一个必然问题:日志无限增长,重放耗时越来越长。
快照的作用是定期把“某一时刻的完整订单簿状态”落盘(或写对象存储),恢复时:
- 先加载最新快照(恢复到某个 sequence_id);
- 再从 sequence_id+1 开始重放增量事件,追平到最新。
工程上需要重点关注:快照频率(RTO vs 运行开销)、快照一致性(不要拿到半更新状态)、以及快照存储介质的可靠性(本地 SSD vs 对象存储)。
5. 适用场景:什么时候值得上“事件溯源 + 快照/重放”
- 低延迟 + 高吞吐是硬指标,无法接受“同步写 DB”把撮合拖慢;
- 状态是可重放的确定性状态机(撮合、风控流水、账户变更等);
- 业务对 RPO/RTO 有明确目标(例如 RPO≈0,RTO 秒级/毫秒级);
- 需要为 HA/热备/异地容灾打基础:备机消费同一事件流即可保持近实时镜像。