18.3 复制日志 (语句 / 行 / 逻辑)
一句话
复制的本质是 “把一次变更写成日志,再在别处重放”;三种格式的差别只有一条:日志里记的是 “怎么做的”(语句)、“改成了什么”(行)、还是 “业务上变了什么”(逻辑)——确定性、体积、跨版本兼容性由此分岔。
三种格式
| 格式 | 记录内容 | 确定性 | 日志体积 | 跨版本/跨库 |
|---|---|---|---|---|
| 语句(STATEMENT) | 原始 SQL 文本 | 差:NOW()、RAND()、LIMIT 无排序、触发器 | 最小 | 差(方言差异) |
| 行(ROW) | 变更行的前后镜像 | 强:只记结果 | 大(大批量更新膨胀) | 中(需表结构一致) |
| 逻辑(LOGICAL) | 逻辑变更(表、列、值、事务边界) | 强 | 中 | 好(可跨版本、可选择性订阅) |
一条 UPDATE 的体积差
UPDATE orders SET status = 'closed' WHERE created_at < '2026-01-01'; -- 影响 10 万行
| 格式 | 日志量 | 说明 |
|---|---|---|
| STATEMENT | 1 条语句 | 但备库执行时 NOW() 等函数可能给出不同结果 |
| ROW | 10 万条行镜像 | 体积大 2–3 个数量级,但结果确定 |
| LOGICAL | 10 万条逻辑变更(可批量压缩) | 订阅方可以只收关心的表 |
确定性:语句复制的四个经典坑
| 坑 | 例子 | 后果 |
|---|---|---|
| 非确定函数 | NOW()、UUID()、RAND() | 主备数据不一致 |
| 无排序的 LIMIT | UPDATE ... LIMIT 100 | 主备更新的行不同 |
| 触发器/存储过程 | 备库再执行一次 | 副作用翻倍 |
| 自增/序列 | 备库序列与主库错位 | 主键冲突 |
这也是 MySQL 默认 binlog_format=ROW 的原因:用体积换确定性。
逻辑日志:CDC 的基础
逻辑日志把 “行变更” 转成 “业务可理解的事件”(table=orders, op=update, pk=42, before={status:'open'}, after={status:'closed'}),因此可以:
- 跨大版本升级(先逻辑复制到新版本,再切换);
- 只订阅部分表(给下游数仓/搜索);
- 被 CDC 工具消费(见 46.4 CDC (变更数据捕获))。
代价是需要主键/复制标识(PostgreSQL 的 REPLICA IDENTITY),否则更新只能全表扫描找行。
代码:同一张表的三种 binlog 形态
-- 数据:orders(id, status, updated_at)
UPDATE orders SET status='closed' WHERE id=42;
-- STATEMENT:只记 SQL 文本(体积最小,依赖执行环境)
-- update orders set status='closed' where id=42
-- ROW:记前后镜像(确定,体积大)
-- ### UPDATE `shop`.`orders` WHERE id=42
-- ### SET @1='closed' ### WHERE @1='open'
-- LOGICAL(Debezium 风格 JSON):记业务语义,可被任意下游消费
-- {"op":"u","before":{"id":42,"status":"open"},"after":{"id":42,"status":"closed"}}
# MySQL:查看当前格式与行镜像策略
mysql -e "SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'binlog_row_image';"
# 判据:binlog_format=ROW 且 binlog_row_image=FULL(或 MINIMAL 但表有完整主键)
反模式
| 反模式 | 后果 |
|---|---|
| 用 STATEMENT 复制却依赖非确定函数 | 主备静默不一致,很久后才发现 |
| ROW 格式但表没主键 | 更新时全表扫描,复制延迟飙升 |
| 逻辑复制忘了设复制标识 | 更新事件缺 before 镜像,下游无法处理 |
| 大事务一次改百万行 | 备库重放卡住,延迟以小时计 |
| 把复制日志当日志分析用 | 格式与保留期都不匹配,解析成本高 |
| 只测主库、不校验备库数据 | 复制错误长期潜伏,切换时爆发 |
| 复制格式与下游解析器版本不匹配 | CDC 消费失败,数仓断流 |
三条铁律
🔑 确定性优先:拿不准就用 ROW;体积可以压缩,不一致不能修复。
🔑 逻辑复制的前提是 “有主键/复制标识”;没有它,变更无法定位到行。
🔑 大事务是复制延迟的头号来源;拆批提交。
自检
Q1 为什么 UPDATE ... LIMIT 100 在语句复制下是危险的?
▸ 答案:因为 LIMIT 不带 ORDER BY 时,返回哪些行是不确定的;主库与备库的执行计划、索引统计、物理顺序不同,可能更新不同的 100 行。语句复制只传 SQL,备库照抄,于是数据分叉。
Q2 ROW 格式日志比 STATEMENT 大很多,为什么仍然值得?
▸ 答案:因为确定性:行镜像记录的是 “结果”,与函数、触发器、执行计划无关,备库重放必然一致。体积问题可以用批量压缩、只记变更列(binlog_row_image=MINIMAL)缓解;而 “主备不一致” 无法自动修复。
Q3 逻辑复制为什么能跨大版本升级?
▸ 答案:因为它传输的是业务语义(表/列/值),不依赖存储格式或事务日志的物理布局。旧版本发布逻辑变更,新版本按自己的格式应用;物理复制则要求主备字节级兼容,跨版本通常不可行。
Q4 一张表没有主键,ROW 复制会有什么性能问题?
▸ 答案:备库应用更新时要定位到具体行:没有主键只能全表扫描匹配所有列,复杂度 O(N) 每行。大表上表现为复制延迟暴涨。解决是加主键(或唯一索引)并重建复制。
Q5 什么时候该用逻辑复制而不是物理复制?
▸ 答案:需要选择性同步(只同步部分表)、跨版本升级、异构下游(数仓、搜索、缓存)时用逻辑复制;要求极致一致、低延迟、同版本时用物理复制(块级、字节一致)。两者可以并存:物理复制做 HA,逻辑复制做数据分发。
Q6 复制日志能当备份用吗?
▸ 答案:不能。复制日志通常只保留有限时间(小时–天),且误删/误更新会同步复制到备库。备份要求 “独立副本 + 可恢复到任意时间点”,见 10.4 快照与备份。
Q7 binlog_row_image=MINIMAL 省了体积,为什么下游 CDC 可能出错?
▸ 答案:因为 MINIMAL 只记被修改的列,before 镜像不完整:下游要按旧值做条件更新、或要还原整行时会缺字段。CDC 场景通常要求 FULL(或至少保证主键 + 变更列齐全),这也是 “体积 vs 可用性” 的又一例。