Binlog:MySQL 主从复制和数据恢复的基础
2026/9/24 5:29:12 网站建设 项目流程

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只记被改的列加上主键,noblobfull的变体,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控制这个缓冲区大小,不够用的时候会溢写到磁盘上的临时文件,事务结束后删掉。

提交的时候分两步:

  1. binlog cache里的内容写进 binlog 文件,这一步是write,还没落盘
  2. sync_binlog决定要不要fsync
    • 0:不主动 fsync,交给操作系统决定,机器掉电会丢
    • 1:每次提交都 fsync,默认值
    • N:累积 N 个事务后 fsync 一次

sync_binlog = 0innodb_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 线程 ↓ 执行到从库
  1. 从库的 IO 线程连上主库,告诉主库"我要从某个 binlog 文件的某个位置开始"
  2. 主库的 dump 线程从那个位置开始读 binlog,推给从库
  3. 从库的 IO 线程收到后写进本地的 relay log
  4. 从库的 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 为什么能够实现崩溃恢复?那篇里提过一半,这里补全。两个日志谁也替代不了谁,有四个原因:

  1. redo log 是循环写的,空间固定,写满就覆盖。它只够支撑"崩溃之后把数据补回来",撑不起"保留过去一个月所有变更"
  2. redo log 属于 InnoDB,MyISAM 之类的引擎没有。复制和恢复是 Server 层的需求,不能绑在某一个引擎上
  3. redo log 是物理逻辑日志,绑死页结构和偏移。换个 MySQL 大版本、换个平台,页的格式可能就变了,日志没法通用
  4. 从库和主库不一定完全一样,页大小、数据文件布局都可能不同,物理日志搬过去对不上

binlog 是逻辑的、归档的、Server 层的,这三条才是复制和恢复需要的性质。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询