TL;DR
- 确定性(Determinism)是撮合引擎的命脉:同一组业务事件必须在任何节点、任何时间重放都得到完全一致的订单簿状态与成交结果。
- 在分布式世界里不能信任物理时间(网络抖动、时钟漂移、乱序),因此需要一个“权威世界线”:把所有指令赋予严格单调递增的全局序列号(Sequencing / Total Order)。
- 工程上最常见的高性能路径是单线程定序 + 事件日志(WAL):定序器只做两件事——排队与编号——把复杂性推给可重放的日志与确定性的撮合状态机。
- 高可用是权衡:主备热切换能保持微秒级延迟但仍有切换窗口;Raft/Paxos 共识能消灭单点但往往会用更高的网络往返来换可用性。
1. 为什么“定序”比“撮合算法”更先决定系统上限?
价格优先、时间优先(Price-Time Priority)看似简单:同价位下谁先到谁先成交。
但在多网关、多机房、跨地域的现实里,“谁先到”并不等价于客户端发起的时间戳。
因为网络延迟、包乱序、NTP 误差会把同一微秒内的并发请求变成一团糟。
所以核心问题并不是“撮合引擎内部怎么比时间”,而是:系统必须在内部建立一个唯一的全序。
一旦全序确定,撮合引擎只需要严格按序消费事件,就能把订单簿当作一个确定性的状态机来运行与重放。
关键要点 / 常见坑(工程视角)
- “用服务器接收时间排序”并不公平:网关/负载均衡路径不同,先到不代表先发;时间戳顶多用于监控延迟,而不是定序依据。
- 定序器不等于撮合:定序器的职责是生成全局序列号 + 写入日志;撮合引擎的职责是按序消费并产生确定性输出,二者分离能显著降低复杂度。
- 没有 WAL 的定序是“写后算”:如果先下发序列再落盘,定序器崩溃会出现“已广播但不可重放”的幽灵事件,导致状态分叉。
- 撮合逻辑里禁用非确定性来源:例如 time.Now()/rand/依赖外部 I/O/遍历无序 map(语言相关),否则重放无法复现。
- 别把共识当银弹:Raft 能给全序,但它也会把“单机内存计数器”变成“多数派网络提交”,延迟曲线会变得更硬。
2. 落地架构:用“全序事件日志”驱动整条交易链路
一个干净的拆分方式是:网关负责并发 I/O,定序器负责全序,撮合引擎负责确定性状态机。
定序后的事件被写入持久化日志(Kafka/Aeron/自研 Journal 皆可),然后撮合、行情、风控、清结算都从同一条有序日志消费。
这样做的好处是:下游不会因为“各自接收顺序不同”而产生数据不一致。
3. 单点定序怎么做到又快又稳?
直觉上“单线程”听起来像瓶颈,但对低延迟系统反而经常是最优解:
单线程天然避免锁竞争、内存屏障与上下文切换,并且更容易把数据留在 CPU cache 里。
工程实践中通常会配合:RingBuffer/无锁队列、CPU 亲和性(pin 核)、批处理写盘(但要控制 tail latency)。
- 输入侧:多网关汇聚到定序器的无锁队列(避免锁争用)。
- 定序侧:递增 sequence + 生成权威时间戳(可用于审计/延迟观测,但不参与排序)。
- 持久化侧:WAL/Journaling 先落盘再对外可见,保证崩溃后可通过日志重放恢复。
- 恢复侧:快照 + 从快照序列号继续重放,快速回到故障前状态。
4. 高可用方案:主备 vs 共识(以及你真正买到的是什么)
定序器是系统“唯一写入者”,天然容易成为 SPOF。
常见落地有两条路:
- Active-Passive 主备热切:正常时按单点最优延迟运行;主节点同时把已定序事件复制给备节点。
关键在于:备节点接管时必须知道“最后一个已持久化的序列号”,避免脑裂与丢单。
- Raft/Paxos 共识定序:用多数派确认来决定全序,集群容忍少数节点故障;代价是每次定序都引入网络回合,通常延迟会显著上升。
选择哪个不是“先进/落后”的问题,而是业务对延迟、可用性、复杂度、成本的权重不同。
5. 适用场景:什么时候你必须认真做定序与可重放?
- 公平性与可审计要求高:需要向监管/客户解释“为什么这笔单先成交”。
- 要做确定性回放/复盘:线上疑难 bug、争议成交、风控穿透都依赖精确重放。
- 多活或跨机房容灾:没有全序就没有一致性,越分布式越需要把“世界线”收拢。
- 下游链路很长:行情、风控、清结算、对账都依赖同一序列,越早统一越省事。