撮合引擎的设计,最核心的权衡就一句话:内存订单簿决定你能跑多快,持久化决定你敢跑多快。 两者的关系不是对立的,而是需要根据交易所的业务规模、资金体量和运维能力,画出一条清晰的边界线。
为什么不干脆全放内存:当性能成为唯一信仰
数字货币和证券交易撮合引擎,本质上是对内存地址的读写,而不是对数据库表的增删改查,行业共识认为,内存操作的速度比磁盘快数个数量级,这才是撮合引擎敢说“单线程撮合百万笔/秒”的底气所在。
内存订单簿的极致形态:零拷贝与无锁队列
订单从网卡进来,到撮合完成并推送回报,全链路不经过任何磁盘IO,业内专家指出,这种设计下,跨机房延迟可以压到微秒级,单核吞吐量足以应对极端的瞬时行情。
- 订单簿使用红黑树或跳表维护价格层级,买一卖一的价格实时变化直接在内存中完成。
- 撮合结果以批量合并的方式写入环形缓冲区,由独立线程异步消费。
但全内存方案的代价同样明显:内存是易失的,进程一崩,订单簿状态直接归零所有未成交的挂单、当前盘口深度、甚至成交回报的顺序,全部化为乌有,这里就引出了一个关键的问题:行情数据是先落库还是先推送给客户端?
从架构上讲,推送发生在撮合引擎内存中,落库发生在消息队列异步消费阶段,如果是先落库,行情推送延迟会显著增加;如果是先推送,万一内存消息积压导致宕机,客户端和数据库的状态就不一致了,行业普遍的做法是先推送给客户端,再异步持久化,用“最终一致性”兜底。
持久化的必要性:从“裸奔”到“穿盔甲”
全内存撮合引擎在演示环境里可以跑得很好,但生产环境必须面对宕机恢复、审计合规和资金清算,这时候,持久化不再是可选功能,而是交易所的生命线。
怎么保证撮合引擎数据不丢失:WAL预写与多级降级
WAL(Write-Ahead Logging)是通用解决方案,核心思想是:任何命令在被执行前,先追加写入日志文件。
在撮合引擎里的实操路径通常是这样:
- 客户端下单指令到达后,先给订单编号,然后追加写入WAL日志。
- 写入成功后再进入内存订单簿撮合。
- 撮合结果(成交回报、撤单结果)再次写入另一组WAL。
- 定期将内存中的订单簿状态做全量快照,并丢弃快照点之前的过期日志。
这个方案的取舍在于刷盘时机,每次下单都强制刷盘(fsync),性能腰斩;批量刷盘(比如每10毫秒或每1000条命令刷一次),又存在断电丢数据的窗口,行业实践中,多数交易所选择“折中策略”定时批量刷盘,同时接受极小概率的最近几百毫秒数据丢失,这个设计,换来了吞吐能力的回退控制在可接受范围内。
订单持久化是“记账”不是“撮合”
要区分清楚,持久化数据库里的订单状态和内存里的订单簿状态,是两套体系。
- 内存订单簿用于快速撮合,是当前可执行的盘口。
- 数据库订单记录用于对账、清算、审计,是历史事实。
架构上最忌把数据库读写放在撮合关键路径上,一旦如此,数据库的锁竞争、磁盘IO抖动都会直接影响撮合延迟,这在行业里是被反复验证过的反面案例。
撮合引擎持久化方案怎么选:从内存快照到WAL追加
选型时,主要考虑三个维度:恢复时间目标(RTO)、数据丢失容忍度(RPO)、以及运维复杂度。
| 方案 | 恢复速度 | 数据丢失范围 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 纯内存 + 冷启动 | 快(秒级) | 全部丢失 | 低 | 测试环境、对账后手动补单 |
| 内存 + 单机WAL | 较快 | 最近一批日志 | 中 | 小规模交易所或做市商内部系统 |
| 内存 + WAL + 异步快照 | 中等 | 极小窗口 | 中高 | 主流数字货币交易所 |
| 内存 + WAL + 多副本同步 | 慢(需协调) | 几乎为零 | 高 | 头部交易所、机构级撮合 |
这个表格不是标准答案,而是一个选择框架。做市商场景下,高频的撤挂单操作会给WAL制造巨大压力,因为每笔撤单也都要记录,面对这种场景,一些系统会引入“合并日志”机制:同价格同数量的连续挂单自动折叠,减少日志量。
内存订单簿多大才算够用:预算与容量规划
订单簿在内存里占用的空间并不大,一个价格档位加上订单链表头,大概几百字节;即使有100万个活跃挂单,也只需要几百MB到1GB左右的内存,真正的内存压力来自于消息积压行情推送队列、风控队列、持久化队列的缓冲。
一个更实际的规划方法是:以最大瞬时并发订单数 × 单订单平均内存占用为基础,再乘以2的冗余系数,据行业通用经验数据,4G内存的撮合节点足以支撑中等规模的合约交易对,但如果你做的是数字货币交易所开发定制价格的对比,会发现不同技术栈的内存管理差异极大Go语言的内存分配器在高并发下比Java更稳定,相对不容易出现GC暂停导致的撮合延迟尖刺。
跨境部署时的地缘考量
如果你的团队在杭州、深圳或新加坡,服务器的物理分布直接决定了撮合引擎和持久化存储之间的延迟,交易撮合必须和行情推送在同一个可用区,否则会出现成交价格落后于盘口显示价格的套利漏洞,这点在考虑“数字货币交易所开发公司哪家好”的时候,是一个经常被问到的差距点,很多外包团队帮你部署系统,但如果不理解同城双活和异地灾备的区别,最终上线后的体验会很不稳定。
做市商场景下的高并发与容灾双压力
做市商是撮合引擎最重要的用户群体,他们的下单行为特点是快速挂单、快速撤单、高并发、大数量级,这恰好是撮合引擎脆弱性的最大暴露面。
快照频率与恢复时间的直接博弈
快照太频繁,占用大量CPU和磁盘IO;快照太稀疏,宕机后重放日志的时间会变长,行业通常的做法:
- 订单成交量达到某一阈值时触发快照。
- 或距离上次快照超过固定时间间隔。
恢复过程就是加载最后一个快照,然后重放快照点之后的WAL日志。
从这个角度说,“币安撮合引擎多久同步一次盘口”这类问题并不准确头部交易所的盘口是在内存中实时流动的,持久化到磁盘的是流水记录,而不是盘口本身。盘口深度数据要的是恢复能力,而不是实时写入能力。
缓存雪崩与订单风暴的防护
做市商策略在重大新闻事件时会同时疯狂撤单或撤所有订单,这时候,如果你在持久化层做了同步调用,系统很容易直接被拖垮,常见的实操防护路径:
- 在撮合引擎前面加自适应节流:活动订单数量超过安全水位时,自动暂停接受非必要的撤单请求。
- 在WAL日志层做分级丢弃:对于明显矛盾的操作指令(先撤单后重新挂单),合并成一条网络操作记录,减少日志量。
- 容灾切流预案:这是相当考验运维功底的环节。
混合架构的终极形态:分层解耦才是答案
不需要在内存和磁盘之间二选一,成熟架构通常是三层:
第一层:内存撮合核心。
买卖盘口、最新成交价、深度数据全部活跃在这里,读写速度最快,这层的性能优化主要围绕缓存行对齐、无锁并发、内存池复用展开。
第二层:异步事件持久化服务。
由独立进程订阅内存核心发出的所有事件,包括订单进入、成交、撤单、拒绝等,然后批量写入数据库或者消息队列。用异步事件日志保证性能和可靠性不被绑定在同一台机器上。
第三层:冷数据归档与离线计算。
用于审计追溯、司法取证和数据挖掘的成交明细与K线数据,冗余存放至多个数据中心,这部分对实时性没有要求,重点是海量数据的长期保存。
在这个架构下,内存订单簿持久化层的关系有点像“前台”和“后台”:前台自由发挥接客,后台稳扎稳打记账。
你所关心的一些细节问题
撮合引擎长期运行会不会内存泄漏?
只要代码规范且使用对象池技术,内存增长趋势是平缓的,建议在监控面板上盯住堆内存占用曲线,一旦出现持续倾斜上升,就需要做heap dump排查。
行情数据是先入库还是先推送给客户端?
先推送,入库是异步的,推送是同步的,如果反过来,你的行情会卡出“一秒一跳”的观感,用户早就跑光了。
如何衡量一个撮合引擎是否优秀?
指标不多:延迟中位数与99%分位延迟、单核吞吐上限、宕机恢复时长、以及价格竞争下的稳定性,能用100万笔/秒来表达的快不过能用500万笔/秒的;但能妥善处理一次宕机不丢数据的,才是真正值得托付的系统。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/632610.html





