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 可用性” 的又一例。

关联