4. 数据库回滚保护:MySQL/MariaDB 的 binlog 与 undo log 机制,回滚点设置

数据库回滚,说白了就是「后悔药」。你执行了一条 DELETE 忘了加 WHERE,或者 UPDATE 把整个表都改了——这时候能救你的,就是 MySQL 底层的两套日志机制。

我刚开始带团队时,有个同事在生产库上跑了个 UPDATE users SET password = '123456',没加 WHERE。嗯,全表 80 万用户的密码全被改了。当时我第一反应不是骂人,而是问:「binlog 开了没?undo 表空间够不够?」幸好都开着,回滚只花了 3 分钟。从那以后,我每次建新库都会亲自检查这两项配置。

4.1 两套日志,各司其职

MySQL/MariaDB 的回滚能力,依赖两个核心组件:binlog 和 undo log。很多人搞混它们,其实分工非常清楚。

对比维度 binlog(二进制日志) undo log(回滚日志)
记录内容 SQL 语句或行变更的逻辑记录 修改前的数据快照(物理/逻辑)
主要用途 主从复制、时间点恢复(PITR) 事务回滚、MVCC 多版本控制
存储位置 磁盘文件(binlog 文件) InnoDB 共享表空间或独立 undo 表空间
生命周期 可配置保留天数,过期自动清理 事务提交后,由 purge 线程异步清理
回滚能力 不支持单条回滚,只能做全量恢复 支持单条语句或整个事务回滚

你想想看,binlog 像不像一个「操作录像机」?它把每一条变更都录下来。而 undo log 更像一个「时光倒流机」,它存的是你修改之前的样子。

核心原则:binlog 负责「重做」,undo log 负责「撤销」。两者配合,才能实现完整的回滚保护。

4.2 undo log 的内部机制

undo log 是 InnoDB 存储引擎独有的。每次你执行 UPDATE 或 DELETE,InnoDB 都会先把旧版本的数据写入 undo log,然后再修改数据页。

举个例子:

-- 原始数据:id=1, name='张三', balance=1000
BEGIN;
UPDATE accounts SET balance = 500 WHERE id = 1;
-- 此时 undo log 中记录了:id=1, name='张三', balance=1000
-- 如果此时 ROLLBACK,InnoDB 直接从 undo log 把旧数据写回去
ROLLBACK;

我在项目中遇到过一个问题:有个报表查询跑了 20 分钟,一直报「undo log 空间不足」。查了半天,发现是一个长事务一直没提交,导致 undo 段无法被 purge 线程回收。嗯,长事务是 undo log 的杀手。

避坑指南:我曾经因为一个未提交的事务,导致 undo 表空间撑爆了磁盘。从那以后,我强制要求所有应用层设置事务超时时间,超过 30 秒的事务自动回滚。

4.3 binlog 的三种格式与回滚场景

binlog 有三种格式,每种对回滚恢复的影响完全不同。

格式 记录方式 回滚恢复特点 推荐场景
STATEMENT 记录原始 SQL 恢复时重新执行 SQL,依赖环境一致 简单场景,不推荐生产
ROW 记录每行变更前后 恢复精确,支持闪回查询 生产环境首选
MIXED 混合模式 自动选择,但恢复不确定性高 谨慎使用

我个人习惯,所有生产库强制使用 ROW 格式。为什么?因为只有 ROW 格式才能做精确的闪回。STATEMENT 格式遇到 NOW() 或 UUID() 这种非确定性函数,恢复出来的数据可能跟原来不一样。

小技巧:如果你用的是 ROW 格式的 binlog,可以用 mysqlbinlog 工具解析出反向 SQL,实现「闪回」效果。我曾经用这个功能,在 5 分钟内恢复了一个被误删的订单表。

4.4 回滚点设置:SAVEPOINT 的使用

MySQL 支持在事务内部设置回滚点(SAVEPOINT)。这玩意儿在复杂事务里特别有用——你不需要回滚整个事务,只需要回滚到某个点。

BEGIN;

INSERT INTO orders (id, amount) VALUES (1, 100);
SAVEPOINT sp1;

INSERT INTO order_items (order_id, product) VALUES (1, '商品A');
-- 这里发现商品A库存不足
ROLLBACK TO SAVEPOINT sp1;
-- 事务回滚到 sp1 点,第一条 INSERT 保留

INSERT INTO order_items (order_id, product) VALUES (1, '商品B');
COMMIT;

你想想看,如果没有 SAVEPOINT,上面这个场景就得整个事务回滚,连订单主表的数据都得重新插入。SAVEPOINT 就像游戏里的存档点,打不过 BOSS 就读取存档,不用重新开号。

注意:SAVEPOINT 只在事务内部有效。事务一旦提交或回滚,所有 SAVEPOINT 自动释放。另外,MySQL 允许设置多个 SAVEPOINT,但名字必须唯一。

4.5 回滚保护的最佳实践

说了这么多原理,最后给几条我在生产环境里验证过的配置建议。

  1. 开启 binlog 并设置 ROW 格式
    # my.cnf 配置
    server-id = 1
    log_bin = /var/log/mysql/mysql-bin.log
    binlog_format = ROW
    expire_logs_days = 7
    max_binlog_size = 1G
  2. 独立 undo 表空间
    # MySQL 8.0+ 默认独立 undo 表空间
    # 旧版本建议迁移
    innodb_undo_tablespaces = 4
    innodb_undo_log_truncate = ON
  3. 监控长事务
    -- 查看运行超过 10 秒的事务
    SELECT * FROM information_schema.INNODB_TRX
    WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 10;
  4. 定期演练回滚流程

    我每季度会在测试库做一次「误操作回滚演练」。模拟 DELETE 忘加 WHERE,然后通过 binlog 做闪回恢复。只有亲手做过,才知道哪些环节容易出问题。

血的教训:我曾经以为 binlog 开了就万事大吉,结果有一次磁盘满了,binlog 写不进去,MySQL 直接挂了。从那以后,我强制监控 binlog 目录的磁盘使用率,超过 80% 就告警。

4.6 核心流程:事务回滚与恢复

下面这张图,把整个回滚流程串起来了。建议你保存下来,以后排查问题对着看。

MySQL 事务回滚与恢复流程 MySQL 事务回滚与恢复流程 用户发起事务 执行 INSERT/UPDATE/DELETE 写入 undo log(旧数据) 写入 binlog(变更记录) 事务提交或回滚? COMMIT:数据持久化 ROLLBACK:从 undo 恢复 undo log 负责事务内回滚,binlog 负责时间点恢复和主从同步

这张图的核心逻辑很简单:写入时双写,回滚时读 undo,恢复时读 binlog。你只要记住这个流程,以后遇到数据库回滚问题,就知道该查哪个日志了。