3. 基于数据库的恢复方案:MySQL/PostgreSQL 的 WAL 日志与回放

聊到订单簿数据恢复,很多人第一反应是「用数据库不就完了?」。嗯,这话对了一半。数据库确实能恢复,但怎么恢复、恢复多快、会不会丢数据,这里面的门道可不少。我个人习惯把数据库恢复方案分成两类:MySQL 系和 PostgreSQL 系。虽然底层原理相似,但实现细节差异很大。

说白了,WAL(Write-Ahead Logging)就是数据库的「黑匣子」。任何修改操作,必须先写日志,再写数据文件。这样即使系统突然崩溃,重启后也能通过回放 WAL 把数据找回来。我在做量化交易系统时,就靠这个机制救回过一次盘中数据——当时机房断电,重启后订单簿数据完好无损。

3.1 WAL 的核心机制

WAL 的工作流程其实不复杂。你想想看,每次下单、撤单、成交,数据库都要改数据。如果每次都直接写磁盘,性能会非常差。所以数据库设计者想了个办法:先把操作记到日志里,然后找个合适的时间再批量写数据文件。

WAL 写入顺序:

  1. 事务开始 → 写入 WAL 日志(LSN 分配)
  2. 数据修改 → 写入 WAL 日志(包含旧值和新值)
  3. 事务提交 → 刷 WAL 到磁盘(fsync)
  4. 后台进程 → 将 WAL 变更应用到数据文件(checkpoint)

这里有个关键点:事务提交成功,只代表 WAL 写完了,不代表数据文件写完了。我在项目中遇到过,有人以为事务提交了就万事大吉,结果磁盘故障导致数据文件损坏,还好 WAL 还在,通过回放恢复了 99.9% 的数据。

3.2 MySQL 的 Binlog 与 Redo Log

MySQL 的 WAL 体系比较特殊,它有两套日志:Redo Log 和 Binlog。Redo Log 是 InnoDB 存储引擎层面的,负责崩溃恢复;Binlog 是 Server 层面的,负责主从复制和时间点恢复。

日志类型 作用 恢复场景
Redo Log 保证事务持久性,崩溃恢复 实例崩溃、断电恢复
Binlog 记录所有修改操作,支持 PITR 误删数据、回滚到指定时间点
Undo Log 事务回滚,MVCC 多版本控制 事务回滚、一致性读

对于订单簿数据恢复,我建议重点关注 Binlog。为什么呢?因为 Redo Log 是循环写的,空间满了会覆盖旧日志。而 Binlog 可以配置为永久保留,方便做时间点恢复。我曾经帮一家交易所做过恢复方案,他们只保留了 Redo Log,结果 Binlog 没开,数据丢失了 2 小时——嗯,这是个惨痛的教训。

MySQL Binlog 回放示例

-- 查看当前 Binlog 状态
SHOW MASTER STATUS;

-- 从指定位置回放 Binlog
mysqlbinlog --start-position=12345 --stop-position=67890 binlog.000001 | mysql -u root -p

-- 基于时间点恢复
mysqlbinlog --start-datetime="2024-01-15 10:00:00" --stop-datetime="2024-01-15 10:30:00" binlog.000001 | mysql -u root -p

避坑指南:我曾经在回放 Binlog 时,没注意字符集问题,导致中文订单备注乱码。建议回放前先检查源库和目标库的字符集是否一致。

3.3 PostgreSQL 的 WAL 与 PITR

PostgreSQL 的 WAL 设计更简洁,一套日志搞定所有事情。它的 WAL 文件默认 16MB 一个,连续编号,可以配置归档策略。我个人比较喜欢 PostgreSQL 的 PITR(Point-In-Time Recovery)机制,恢复粒度可以精确到事务级别。

为什么会这样?因为 PostgreSQL 的 WAL 记录了每个事务的起始和结束标记。回放时,你可以指定恢复到某个事务之前,或者跳过某个有问题的操作。这在处理订单簿数据时特别有用——比如某个错误订单导致数据不一致,你可以回放到该订单之前的状态。

PostgreSQL WAL 配置示例

-- postgresql.conf 关键配置
wal_level = replica           -- 开启 WAL 归档所需级别
archive_mode = on             -- 开启归档模式
archive_command = 'cp %p /archive/%f'  -- 归档命令
max_wal_senders = 5           -- 最大 WAL 发送进程数

-- 查看当前 WAL 位置
SELECT pg_current_wal_lsn();
SELECT pg_walfile_name(pg_current_wal_lsn());

PITR 恢复步骤

  1. 停止数据库服务
  2. 备份当前数据目录(以防万一)
  3. 清理数据目录,恢复基础备份
  4. 配置 recovery.conf,指定恢复目标
  5. 启动数据库,自动回放 WAL
-- recovery.conf 配置示例
restore_command = 'cp /archive/%f %p'
recovery_target_time = '2024-01-15 10:25:00'
recovery_target_action = 'promote'

注意:PITR 恢复时,如果 recovery_target_time 指定的时间点恰好在一个长事务中间,PostgreSQL 会回滚该事务。这意味着订单簿中该事务的所有修改都会丢失。我建议在恢复前先确认目标时间点是否安全。

3.4 订单簿场景下的 WAL 优化

订单簿数据有个特点:写入频繁、单条数据小、对延迟敏感。默认的 WAL 配置可能不够用。我总结了几条优化经验:

  • 调整 WAL 缓冲区大小:MySQL 的 innodb_log_buffer_size 建议设为 64MB 以上,PostgreSQL 的 wal_buffers 建议设为 16MB。太小会导致频繁刷盘,影响性能。
  • 使用 SSD 存储 WAL:机械硬盘的随机写入性能太差。我在项目中把 WAL 单独放在 NVMe SSD 上,写入延迟从 5ms 降到了 0.1ms。
  • 合理设置 checkpoint 间隔:MySQL 的 innodb_log_file_size 和 PostgreSQL 的 checkpoint_timeout 要平衡。太频繁影响性能,太稀疏恢复时间变长。
  • 开启 WAL 压缩:PostgreSQL 支持 wal_compression,可以减少磁盘占用和 IO 开销。MySQL 8.0 也支持 Binlog 压缩。

3.5 恢复演练与验证

光有方案不行,得定期演练。我见过太多团队,方案写得天花乱坠,真出问题时才发现恢复不了。建议每季度做一次恢复演练,验证以下内容:

  1. WAL 归档是否完整,有没有丢失文件
  2. 恢复后的数据是否一致,订单簿的买卖盘是否平衡
  3. 恢复时间是否在 SLA 范围内(通常要求 15 分钟内)
  4. 主从切换后,新主库的 WAL 是否连续

我的经验:有一次演练发现,归档脚本中 cp 命令没加 -p 参数,导致文件权限丢失,恢复时数据库无法读取 WAL。从那以后,我要求所有归档命令必须保留文件权限和所有权。

3.6 知识体系总览

下面这张图总结了基于数据库的 WAL 恢复方案的核心逻辑。你可以看到,从数据写入到崩溃恢复,整个链路是闭环的。

基于数据库的 WAL 恢复方案核心逻辑 应用层(订单簿) 事务提交 WAL 写入(先写日志,后写数据) 正常:数据写入数据文件 异常:崩溃/断电 恢复:回放 WAL 日志 核心原则:WAL 必须在数据文件之前持久化到磁盘

嗯,以上就是基于数据库的 WAL 恢复方案的核心内容。说白了,WAL 就是数据库的「后悔药」,但前提是你得把药备好、知道怎么吃。我个人建议,无论用 MySQL 还是 PostgreSQL,都要把 WAL 归档和定期演练纳入日常运维流程。别等到数据丢了才想起来——那时候就晚了。

专注资料整理