数据持久化策略:全量快照 vs 增量日志,如何选择

好,咱们来聊聊这个核心问题。订单簿的数据恢复,说白了就两个路子:要么定期拍一张「全家福」,要么把每笔操作都记下来。我做了这么多年交易系统,这两种方案都踩过坑,今天把经验摊开来讲。

先搞清楚:我们到底在恢复什么?

订单簿的数据,本质上是一个内存中的状态机。每一笔订单的进入、撮合、撤单,都在改变这个状态。系统崩溃后,我们要重建的,就是崩溃前最后一刻的那个状态。

你想想看,如果每秒有10万笔订单进来,内存里维护着几十万个限价单。突然断电了,怎么办?

嗯,这就是持久化要解决的问题。

方案一:全量快照

全量快照,就是每隔一段时间,把整个订单簿的状态完整地写下来。比如每5分钟、每10分钟,把当前所有的订单数据序列化到磁盘。

核心思路: 用空间换时间。恢复时直接加载最近一次快照,一步到位。

优点很明显:

  • 恢复速度极快。加载一个文件,内存里就重建好了
  • 逻辑简单,不容易出错
  • 没有依赖关系,随便哪个快照都能独立使用

缺点也扎心:

  • 数据量大。一个订单簿快照可能几十MB甚至上百MB
  • 写入频繁时,磁盘I/O扛不住
  • 快照间隔内发生崩溃,会丢失这段时间的数据

我记得有一次,在某个交易所做优化。他们每1分钟打一次快照,结果磁盘直接被打满了。后来改成5分钟一次,配合增量日志,才把问题解决。

方案二:增量日志

增量日志,就是只记录「变化」。每来一笔订单,就写一条日志:ADD order_id=12345, price=100.5, qty=100。每撮合一笔,也写一条:TRADE order_id=12345, filled_qty=50

核心思路: 用时间换空间。恢复时从某个起点开始,逐条回放日志。

优点:

  • 写入量小。每条日志可能就几十字节
  • 可以做到几乎不丢数据(配合WAL机制)
  • 方便审计和回放

缺点:

  • 恢复慢。如果日志积累了几小时甚至几天,回放时间会很长
  • 日志文件管理复杂,需要做日志滚动和清理
  • 如果日志本身损坏,恢复就麻烦了

我曾经遇到过一个极端案例:某系统连续运行了72小时没打快照,日志文件堆了200GB。结果系统崩溃后,光回放日志就花了40分钟。你说这能忍吗?

那到底怎么选?

别急,我画了一张图,帮你理清思路。

全量快照 vs 增量日志:选择决策树 数据持久化策略 全量快照 增量日志 恢复快(秒级) 磁盘占用大 可能丢数据 几乎不丢数据 写入量小 恢复慢(分钟级) 最佳实践:全量快照 + 增量日志 混合使用 定期快照 + 快照之间的增量日志,兼顾恢复速度和数据完整性

我的建议:别二选一,混合才是王道

说实话,在真正的生产环境里,很少有人只用一种方案。我参与过的几个大型交易系统,无一例外都是全量快照 + 增量日志的混合模式。

具体怎么做?

  1. 定期打快照:比如每5分钟或每10分钟,把订单簿状态完整写下来
  2. 持续写增量日志:快照之间的所有操作,都记录到日志里
  3. 恢复时先加载快照,再回放日志:这样既快又不丢数据
一个小技巧: 快照的生成可以在后台异步进行,不影响主线程的订单处理。我习惯用双缓冲技术,一个缓冲区在写快照,另一个继续处理订单。

代码示例:快照与日志的混合实现

下面是一个简化版的实现思路,用伪代码展示核心逻辑:

// 快照生成
void take_snapshot(OrderBook* book) {
    // 1. 暂停增量日志写入(短暂锁住)
    lock(log_mutex);
    
    // 2. 序列化当前订单簿
    serialize(book, "snapshot_20240101_120000.dat");
    
    // 3. 记录当前日志位置
    uint64_t log_pos = get_log_position();
    save_checkpoint(log_pos);
    
    // 4. 释放锁,继续处理订单
    unlock(log_mutex);
    
    // 5. 清理旧的日志文件(保留快照之后的即可)
    clean_old_logs(log_pos);
}

// 数据恢复
void recover(OrderBook* book) {
    // 1. 加载最近一次快照
    load_snapshot(book, "snapshot_20240101_120000.dat");
    
    // 2. 获取快照时的日志位置
    uint64_t checkpoint = load_checkpoint();
    
    // 3. 从该位置开始回放日志
    replay_logs(book, checkpoint);
    
    // 4. 恢复完成,开始正常处理
    printf("恢复完成,共回放 %d 条日志\n", log_count);
}

避坑指南

我曾经踩过的坑:
  • 快照写入时没有加锁,导致快照和日志不一致。恢复出来的订单簿少了几个订单
  • 日志文件没有做CRC校验,磁盘坏道导致日志损坏,恢复失败
  • 快照频率太高,磁盘I/O成为瓶颈,影响了正常订单处理

嗯,这里要注意一点:快照的间隔不是越短越好。我一般建议根据订单量来动态调整。比如每秒10万笔订单时,5分钟一次快照就够了。如果每秒只有1000笔,那30分钟一次也行。

表格对比:一目了然

维度 全量快照 增量日志 混合方案
恢复速度 快(秒级) 慢(分钟级) 较快(秒级+少量回放)
数据完整性 可能丢失快照间隔内的数据 几乎不丢数据 几乎不丢数据
磁盘占用 中等
实现复杂度
适用场景 低吞吐、可接受少量丢数据 高吞吐、不能丢数据 高吞吐、高可用要求

最后说两句

其实选择哪种策略,最终取决于你的业务容忍度。如果丢了1秒的订单数据,客户会骂娘吗?如果恢复需要10分钟,交易员能接受吗?

我个人习惯是:宁可多花点磁盘空间,也要保证恢复速度和数据完整性。毕竟在金融系统里,数据就是钱,时间就是命。

好了,关于全量快照和增量日志的选择,就聊到这里。记住,没有银弹,只有最适合你场景的方案。


专注资料整理