binlog 是什么
binlog 是 Server 层的归档日志,记的是"数据发生了哪些变更"。
和 redo log 摆在一起看,差别最明显的是三点:
- redo log 是循环写的,空间固定,写满就覆盖;binlog 是追加写的,写完一个文件换下一个,历史都在
- redo log 属于 InnoDB,MyISAM 根本没有;binlog 属于 Server 层,用什么引擎都有
- redo log 是物理逻辑日志,绑死在页和偏移上;binlog 是逻辑日志,记的是语句或者行
前两点决定了 binlog 能长期保留、能跨引擎,第三点决定了它能跨版本、跨机器用。主从复制和按时间点恢复这两件事,只有 binlog 干得了。
三种格式
binlog_format有三个取值,这是 binlog 最需要搞清楚的地方。
STATEMENT
记原始的 SQL 语句。UPDATE t SET name = 'b' WHERE id = 1记的就是这条 SQL 本身。
好处是日志小。一条UPDATE改了 100 万行,也只需要记一条语句带来的几十个字节。
坑在于不确定的语句会让主从不一致:
UPDATEtSETcreate_time=NOW()WHEREid=1;UPDATEtSETtoken=UUID()WHEREid=1;UPDATEtSETn=n+1WHEREid=1LIMIT1;主库执行的时候NOW()是 10:00:00,binlog 里记的是NOW()这个函数调用,从库回放的时候是 10:00:05,写进去的时间就不一样了。UUID()、RAND()同理,LIMIT不带ORDER BY的时候取哪一行也不确定。
这类语句在主从环境里是隐藏的雷,等到发现数据对不上往往已经过了很久。
ROW
不记 SQL,记每一行改前和改后的值。
UPDATE t SET name = 'b' WHERE id = 1在 ROW 格式下会变成两个事件:
Table_map_event 记录涉及的表结构,表 id、列数、每列的类型 Update_rows_event 记录 id = 1 这一行,name 从 'a' 变成 'b'Table_map_event是给从库看的,因为从库要按列的位置来解析行数据,得先知道表长什么样。
好处是结果确定。不管 SQL 里写了NOW()还是UUID(),binlog 里存的是最终写进去的那个值,从库照着写就行,不可能不一致。
代价是日志大。一条UPDATE改 100 万行,就是 100 万行的前后值,日志体积可能比 STATEMENT 大几十倍。所以 ROW 格式下max_binlog_size会经常触发文件切换,磁盘占用也涨得快。
MySQL 5.7.7 之后默认就是 ROW 格式,这也是现在的主流选择。
ROW 格式还有个参数
binlog_row_image控制记多少列:full(默认)记所有列的前后值,minimal只记被改的列加上主键,noblob是full的变体,BLOB 和 TEXT 列只在真的被改时才记。写多读少的场景调成minimal能省不少空间。
MIXED
MySQL 自己判断,默认用 STATEMENT,碰到不确定的函数就切到 ROW。
听起来两全其美,实际用的人不多。因为哪些语句算"不确定"由 MySQL 判断,判断的边界不透明,出了问题不好排查,不如直接上 ROW 求个确定。
怎么选
| 格式 | 日志体积 | 主从一致性 | 说明 |
|---|---|---|---|
STATEMENT | 小 | 有隐患 | 除非有明确的体积压力,不建议 |
ROW | 大 | 确定 | 现在的主流,5.7.7 之后是默认值 |
MIXED | 中 | 依赖判断 | 判断边界不透明,用得少 |
一句话,用磁盘空间换确定性,选 ROW。
写盘的时机
binlog 的写入路径比 redo log 简单一层:
事务执行 → binlog cache(线程私有)→ binlog 文件 → 磁盘注意binlog cache是每个线程一份的,不像 redo log buffer 是全局共享的。原因是一个事务的 binlog 必须连续完整地放在一起,如果所有线程共用一个缓冲区,两个事务的 binlog 会交织在一起,从库没法按事务回放。
binlog_cache_size控制这个缓冲区大小,不够用的时候会溢写到磁盘上的临时文件,事务结束后删掉。
提交的时候分两步:
- 把
binlog cache里的内容写进 binlog 文件,这一步是write,还没落盘 - 按
sync_binlog决定要不要fsync0:不主动 fsync,交给操作系统决定,机器掉电会丢1:每次提交都 fsync,默认值N:累积 N 个事务后 fsync 一次
sync_binlog = 0和innodb_flush_log_at_trx_commit = 2有点像,都是数据交给 OS 了但没落盘,进程崩溃不丢、机器掉电会丢。
线上最严的配置是sync_binlog = 1加上innodb_flush_log_at_trx_commit = 1,俗称双 1,含义是"提交返回了就一定不会丢"。
还有一条规则:一个事务的 binlog 不能被拆到两个文件里。binlog 文件按max_binlog_size(默认 1GB)切换,但如果切换的时候有事务正在写,会等这个事务写完再切。所以实际的文件大小可能略超过 1GB。
主从复制靠它
复制的整个机制就是"把主库的 binlog 搬到从库执行一遍",中间靠三个线程接力:
主库 从库 ┌─────────────┐ ┌──────────────────┐ │ binlog │ │ relay log │ └─────────────┘ └──────────────────┘ ↑ ↑ ↓ dump 线程 ──── 推 binlog ────→ IO 线程 SQL 线程 ↓ 执行到从库- 从库的 IO 线程连上主库,告诉主库"我要从某个 binlog 文件的某个位置开始"
- 主库的 dump 线程从那个位置开始读 binlog,推给从库
- 从库的 IO 线程收到后写进本地的 relay log
- 从库的 SQL 线程读 relay log,在从库上把事件重放一遍
为什么中间要落一份 relay log,不直接从 IO 线程交给 SQL 线程?为了让两个线程解耦。SQL 线程回放慢的时候不会卡住 IO 线程继续拉取,主库挂了从库手里也有存货能继续回放。另外 SQL 线程可以并行,IO 线程只能串行拉,分开才好做并行复制。
数据恢复靠它
有了全量备份加上 binlog,就能恢复到任意一个时间点,这个叫PITR( Point-In-Time Recovery )。
流程是两步:先把最近一次全量备份恢复回去,再把备份之后到目标时间点之间的 binlog 重放一遍。
mysqlbinlog\--start-datetime="2026-09-23 10:00:00"\--stop-datetime="2026-09-23 10:30:00"\--database=test\mysql-bin.000001|mysql-uroot-p举例:凌晨 2 点做了全量备份,上午 10 点有人误删了一张表,想恢复到 10 点之前。那就恢复备份,然后把凌晨 2 点到 9 点 59 分之间的 binlog 重放一遍。这也是为什么误删数据之后第一件事不是着急操作,而是先把 binlog 保护起来,因为它是唯一能找回数据的凭据。
--start-datetime和--stop-datetime可以换成--start-position和--stop-position,用精确的位点,比时间更准。时间点恢复的时候要找"误操作之前的那个位点",通常用--stop-position卡住。
ROW 格式的 binlog 直接看是二进制,要用
mysqlbinlog -vv解码。-v会把行变更转成伪 SQL,-vv还会带上列的类型注释,排查问题的时候基本都用-vv。
为什么有了 redo log 还要 binlog
这个问题在 Redo Log:MySQL 为什么能够实现崩溃恢复?那篇里提过一半,这里补全。两个日志谁也替代不了谁,有四个原因:
- redo log 是循环写的,空间固定,写满就覆盖。它只够支撑"崩溃之后把数据补回来",撑不起"保留过去一个月所有变更"
- redo log 属于 InnoDB,MyISAM 之类的引擎没有。复制和恢复是 Server 层的需求,不能绑在某一个引擎上
- redo log 是物理逻辑日志,绑死页结构和偏移。换个 MySQL 大版本、换个平台,页的格式可能就变了,日志没法通用
- 从库和主库不一定完全一样,页大小、数据文件布局都可能不同,物理日志搬过去对不上
binlog 是逻辑的、归档的、Server 层的,这三条才是复制和恢复需要的性质。