VOL.001 · 创刊号Token 天团 API

Token 天团

—— 一份关于代码与生活的个人刊物 ——

技术Java教程

redo / undo / binlog 三本账:崩溃不丢、回滚可退、复制有据——Java 工兵连第 12 篇

「Java 工兵连」第 12 篇,接着第 11 篇的 undo 链往深处走。新手常见三个问题:为什么改数据先写日志(不更慢吗)?为什么日志要三条、一条不行吗?主从数据为什么会不一致?答案全在三本账的分工里。

一、先建直觉:一家饭店的账

  • 数据页(Buffer Pool 里的脏页)= 柜台上的账本原件——改完先放着,打烊后(后台线程)才誊进保险柜的正式账本(磁盘数据文件);
  • redo log = 柜台边的流水便签:每笔交易先草记一笔。突然停电(崩溃)?凭便签把没誊完的账一笔笔补上——崩溃不丢;
  • undo log = 退货登记簿:每笔交易都顺手记下「这笔怎么撤」。顾客要退货(回滚),照簿反向操作;老顾客要查上个月的账(MVCC 读历史),也翻这本簿——可反悔、可查史;
  • binlog = 上报总店的正式流水册:按时间顺序追加、只增不改,定时抄送各分店(从库复制)、永久存档(时间点恢复)——传承有序。

三句职责口诀就是标题那句:redo 保崩溃,undo 保后悔,binlog 保传承。

二、三本账对比:一张表看全

三本账卡片 + 一次 UPDATE 的两阶段提交时序

redo log undo log binlog
所属层 InnoDB 引擎 InnoDB 引擎 MySQL Server(所有引擎)
记什么 物理页修改 逻辑反操作 逻辑事件(ROW/STMT/MIXED)
写的时机 事务执行中先写(WAL) 修改前先写 提交时写
空间方式 固定大小,循环写 按需增长,purge 清理 追加写,永不覆盖
崩溃之后 重放「已提交」的 回滚「没提交」的 已落笔的不动
拿来干嘛 崩溃恢复 回滚 + MVCC 主从复制、按时间点恢复

三个「为什么」从表里直接读出来:

为什么改数据先写日志反而快?(WAL 的灵魂)改数据页是随机写(页散落在磁盘各处),写 redo 是顺序写(文件末尾追加)。顺序写比随机写快几个数量级——先把便宜的做了,贵的(刷脏页)交给后台慢慢来。「先记便签后誊账,随机写变顺序写」,这个思想 Kafka 的顺序写、LSM 树(No.18、No.19 还会遇到)全是同门。

为什么 redo 循环写、binlog 追加写? redo 只管「最近的账」:便签写满了就必须逼着柜台把旧账誊完(推 checkpoint 强制刷脏页),旧便签即可覆盖——所以 redo 不能做时间点恢复。binlog 是存档,只增不改——所以能按时间点恢复(把备份 + binlog 重放到任意时刻,「误删表抢救」全靠它)。

为什么 binlog 默认 ROW 格式? STATEMENT 记原始 SQL,省空间,但 now()、uuid()、LIMIT 这类不确定语句在主从会执行出不同结果;ROW 记行镜像(改前改后各一份),笨但绝对一致。默认的安全换空间,没毛病。

三、两阶段提交:redo 和 binlog 怎么不打架

三条日志里有一致性风险的是 redo 和 binlog:一个在引擎层、一个在 Server 层,两次落笔之间崩溃怎么办? 只写了一边,主库恢复后和从库(靠 binlog 同步)数据就岔了。

InnoDB 的答案是内部 XA · 两阶段提交(见 hero 图时序):

  1. redo prepare——物理修改落 redo,状态标记「预备」;
  2. 写 binlog——总账落笔;
  3. redo commit——盖章,事务完成。

崩溃恢复规则:重启后扫描 redo,发现停在 prepare 状态的事务 → 去查 binlog:binlog 里有完整记录 → 提交;没有 → 回滚。一句口诀:先预备、再记总账、最后盖章;恢复时看总账定生死。这样无论崩在哪一步,主从永远一致。

四、实战:双 1 与大事务

刷盘档位是生产上最常被问的取舍,两个参数一乘:

innodb_flush_log_at_trx_commit = 1   # redo:每次提交都 fsync 到磁盘
sync_binlog = 1                      # binlog:每次提交都 fsync 到磁盘
  • 双 1:最稳,提交即落盘,崩溃一条不丢——交易、支付类库的底线;
  • 双 0:最快(攒一批再刷),崩溃可能丢最近几秒的事务——日志库、可重算的场景可用;
  • 1×2 或 2×1:折中,MySQL 崩溃不丢、操作系统崩溃可能丢——看似安全实则最尴尬的档位,要么稳到双 1,要么明确接受丢失,别选半吊子。

大事务与主从延迟:从库回放 binlog 是单事务串行的——一个跑了几分钟的大事务在 binlog 里是一个巨型事件,从库要整体回放完才继续下一个,延迟瞬间拉高(还能把从库 OOM——第 10 篇的大对象暗线又来了)。处方和第 11 篇一模一样:拆事务。批量更新按千行一批分段提交,主从延迟立刻回落。一个坏习惯,两篇的病。

老兵点评

  1. 把 WAL 当成一种通用思维,而不只是 MySQL 名词。先写便宜的顺序日志、再异步做昂贵的随机落盘——Kafka、Elasticsearch、LevelDB、文件系统的 journal,全是这一招。看懂 redo,你看一半存储系统都开了透视。
  2. 「双 1」不是性能问题,是品味问题。我见过太多库「为了性能」调成双 0,出事后才想起这是钱。真正的性能优化路径是:拆大事务、合并小提交(组提交)、升级磁盘——而不是关保险。
  3. 面试段位差:初级背「三种日志的作用」;中级讲 WAL 和两阶段提交流程;高级讲 redo 物理与 binlog 逻辑的差异、循环写与追加写的代价、以及组提交(group commit)怎么把多次 fsync 合并成一次——到这层你在聊存储引擎设计,不是背书。

带走三句话

  1. redo 保崩溃(物理、循环写、WAL 先写),undo 保后悔(逻辑反操作 + MVCC 版本链),binlog 保传承(逻辑、追加写、复制恢复)。
  2. 一致性靠两阶段提交:redo prepare → binlog → redo commit;崩溃恢复看 binlog 定生死。
  3. 刷盘口诀:双 1 最稳、双 0 最快、半吊子别选;主从延迟第一嫌疑人永远是大事务。

本篇是「Java 工兵连」第 12 篇。→ 上一篇:第 11 篇 · 事务隔离与 MVCC | 继续阅读:第 13 篇 · MySQL 锁全景与死锁排查 | 浏览整个专栏 →。

❦ 终 ❦

← 回到首页全部归档 →