MySQL binlog格式怎么选?ROW、STATEMENT、MIXED的区别与坑

大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。

有回对账,主从两边的总额差了十几万。行数也少了,主库明明有那么多。我先怀疑复制断了。SHOW REPLICA STATUS 一看,延迟是 0,线程都正常。查到最后,是 binlog 格式的锅。那条语句里有 LIMIT,又没有 ORDER BY。主库和从库各挑了 100 行,挑的不是同一批。

在那之前我一直以为,binlog 记的就是「执行了什么 SQL」。从库照着跑一遍就完事。后来才明白,记过程还是记结果,是两套完全不同的账。

三种格式,记的到底是过程还是结果

STATEMENT 记 SQL 原文。一条 UPDATE 影响五千行,binlog 里也只有那一行语句。到了从库,这条语句被重新执行一遍。

ROW 记每行的前后镜像。哪一行从什么改成什么,一条一条写清楚。从库不重新执行 SQL,照着改行就行。不过 ROW 只管 DML。建表、加索引这类 DDL,照样记成语句事件。

MIXED 默认走 STATEMENT,碰到它认为不安全的语句,临时切到 ROW。听起来聪明,实际是两套逻辑都要维护。一句话,STATEMENT 重放的是过程,ROW 重放的是结果。过程能不能重放出一样的结果,全看语句是不是确定的。

先纠正一个常见误解

很多人一听 STATEMENT 就摇头,说 NOW() 会不一致。这个说法不准。主库写 binlog 之前,会先记一个 SET TIMESTAMP。从库执行时用的是这个时间戳。所以 NOW() 在主从两边是一样的。

真正会出岔子的是 SYSDATE()。它不看 SET TIMESTAMP,谁执行就取谁当时的系统时钟。主从机器时钟差一点,结果就差一点。

sql 复制代码
-- 主库和从库各执行一次,NOW() 一致,SYSDATE() 可能差出几百毫秒
SELECT NOW(), SYSDATE();

还有几类更直接。UUID() 和 RAND() 每次执行都是新值。LIMIT 不带 ORDER BY,取哪几行没保证。用户变量、自定义函数,也可能在两边算出不同结果。

语句 STATEMENT 下 原因
NOW() 一致 主库把时间戳写进了 SET TIMESTAMP
SYSDATE() 可能不一致 读执行时刻的系统时钟
UUID()、RAND() 不一致 每次执行都是新值
LIMIT 无 ORDER BY 可能不一致 挑哪几行没有保证
用户变量 可能不一致 依赖上一条语句的结果

复现一次很简单。建一张带令牌的表,插入一行:

sql 复制代码
CREATE TABLE t_claim (
  id INT PRIMARY KEY AUTO_INCREMENT,
  token VARCHAR(36),
  created DATETIME
);
INSERT INTO t_claim(token, created) VALUES (UUID(), NOW());

主库插一条,从库重放同一条语句,UUID() 会再算一次。两边行数一样,token 不一样。用 checksum 一对,立刻露馅。这类不一致最难受的地方,是它不报错,也不影响查询。账面上安安静静,数据已经分家了。

体积这笔账,ROW 确实更贵

同一张表,同样五千行更新,三种格式的 binlog 体积差得很远。下面的数字是我搭的示意场景,换到你的表以实测为准。

格式 同一条五千行 UPDATE 的 binlog 体积(示意) 特点
STATEMENT 约 0.1 KB 一行原文,体积最小
MIXED 约 0.1 KB 确定性语句跟 STATEMENT 一样
ROW 约 1.2 MB 每行的前后镜像都要记

ROW 的代价不只落在磁盘。网络带宽、从库回放、备份体积、CDC 解析,全跟着涨。批量更新越大,差距越明显。

还有一个隐性成本,落在回放上。从库应用行事件时,要靠主键或唯一键定位那一行。表上没有主键,它只能全表扫描去找。一条一条扫,主从延迟就上来了。

想自己量,别猜。先用 FLUSH BINARY LOGS 切一个新文件。跑完同一批语句,再看 SHOW BINARY LOGS 里新文件的大小。

sql 复制代码
FLUSH BINARY LOGS;
SHOW BINARY LOGS;

省空间的开关有三个。binlog_row_image=MINIMAL 只记发生变化的列。binlog_transaction_compression=ON 在 8.0.20 之后可以开,事务级压缩,落盘更小。排查时想看原语句,就打开 binlog_rows_query_log_events,ROW 事件里会带上原始 SQL。

sql 复制代码
SHOW VARIABLES LIKE 'binlog_row_image';
SHOW VARIABLES LIKE 'binlog_transaction_compression';
SHOW VARIABLES LIKE 'binlog_rows_query_log_events';

MINIMAL 要慎用。前后的完整镜像没了,同步工具和闪回工具会读不到旧值。改之前先确认依赖它的工具能不能扛。

还有一个更隐蔽的代价,落在并行复制上。WRITESET 靠每行的唯一键值算哈希。写集有重叠,两个事务才算冲突。MINIMAL 只留定位列和变更列,写集就可能算不全,甚至为空。表上没有主键或唯一键,也是一样的结果。

官方对这类事务的处理,是回落到 COMMIT_ORDER。格式还是 ROW,但并行度按提交顺序算,不再按写集算。批量更新越密集,这层损失越明显。要用 WRITESET,就把 binlog_row_image 留在 FULL。想省空间,优先开事务压缩,别从行镜像上抠。

8.0 为什么把 ROW 设成默认

默认值不是拍脑袋定的。5.7.7 之后,ROW 就是默认格式。到 8.0.34,binlog_format 这个参数本身被标记弃用,官方方向是以后只保留 ROW。

原因就三条。第一,一致性优先。重放结果比重放过程可靠,尤其面对复杂语句和非确定性函数。第二,并行复制。按行算冲突的 WRITESET,依赖 ROW 格式。第三,生态。CDC、审计、闪回这些工具,基本只认 ROW 事件。

所以 ROW 的代价是明码标价的。空间、带宽、回放开销,都算得出来。STATEMENT 省下来的空间,赌的是一个前提。这条语句在两边一定算出同一个结果。赌对了省很多,赌错了要拿数据去补。

MIXED 的边界,比想象中窄

MIXED 的自动切换,靠的是对语句「是否安全」的判断。判断是启发式的,不是证明。碰到它没识别出来的写法,照样按 STATEMENT 记。

还有一层麻烦。MIXED 的 binlog 里,两种事件混着出现。解析工具得同时处理 Query 事件和行事件,少支持一类就漏数据。CDC 场景尤其明显,很多工具直接要求全 ROW。MIXED 看着折中,实际是两套逻辑都得懂。除非你在老版本上被空间卡得没办法,新系统没必要选它。

怎么选,看这张表

场景 建议
新系统、8.0 之后 直接用 ROW,别改
有 CDC、审计、闪回、数据同步 必须 ROW
要用并行复制、WRITESET 用 ROW,行镜像留 FULL
带宽极紧、语句全是确定的、没有同步需求 才考虑 STATEMENT
想省空间又怕不一致 别选 MIXED,选 ROW 加压缩;要 WRITESET 就留 FULL

切换时的坑

最容易犯的,是只改主库。主库切了 ROW,从库还留着老设置,表面上照常复制。真到做主从切换、加级联从库的时候,问题才冒出来。稳妥做法是主从一起改,写进配置文件,重启生效。也别在业务高峰切。改完先看一眼新事务的格式,确认不是只改了会话。

sql 复制代码
-- 看事件类型:Query 是语句事件,Table_map、Write_rows 等是行事件
SHOW BINLOG EVENTS IN 'mysql-bin.000123' LIMIT 20;

-- 想看行事件的具体内容,用 mysqlbinlog 解
-- mysqlbinlog -vv --base64-output=DECODE-ROWS mysql-bin.000123

还有一件事得说清楚。切格式只能防止新的不一致,修不了已经错的数据。切之前先对一次账,切之后再对一次。两次都干净了,才算真的落地。

它选的不是速度,是赌注

三种格式比的不是谁快,是你愿意为什么买单。STATEMENT 买空间,卖的是确定性。ROW 买确定性,卖的是空间和带宽。MIXED 想两头占,实际把判断成本留给了你。

所以别问哪个格式最好。先问自己三件事。这条链路要不要 CDC,要不要并行复制。语句里有没有 SYSDATE、UUID、不带排序的 LIMIT。答案清楚了,格式自然就定了。

避坑清单

格式要看运行时的值,别看配置文件。SELECT @@binlog_format; 最准。配置文件改了没生效、会话被单独改过,这些情况都常见。

STATEMENT 下别赌函数。NOW() 有 SET TIMESTAMP 兜着,SYSDATE()、UUID()、RAND() 都没有。带 LIMIT 的更新,先补上 ORDER BY 再谈格式。

最后一条是我自己踩的。为省 binlog 空间,我把清理任务那台库切成了 STATEMENT。那条 UPDATE 带 LIMIT,没排序。跑了一周,从库比主库多留了一批该删的行。对账发现时,业务已经在用从库读数据了。现在清理任务一律按主键分批,格式也改回了 ROW。

写在最后

binlog 是写给未来的。不是为了今晚的主从同步,是为了半年后有人问起那天。那天到底改了什么,改的是哪几行。答案还在不在,取决于你今天选了哪种格式。

数据对不上最麻烦的地方,是它不报警。查询照常返回,报表照常出数。错的那几行安安静静躺着,没人喊,也没人疼。等有人发现,往往已经错了很久。磁盘满了可以扩,带宽紧了可以加,机器坏了可以切。这些都有告警。账错了却没人报。它不像故障,更像一笔慢慢漏掉的零钱。

备份能把数据倒回昨天,binlog 才能说清昨天怎么过的。所以这道题不考参数记不记得住。考的是你愿不愿意为「以后说得清」多付一点。空间今天花出去,明天还看得见。现场今天省下来,以后就再也拼不回去了。我今天还是选 ROW。不是因为它便宜,是因为它贵在明处。

你们生产用的是哪种 binlog 格式?有没有被 STATEMENT 坑过?评论区聊聊。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

相关推荐
小冯不疯1 小时前
查询旺店通货品策略实战教程:从源系统拉取到轻易云的端到端落地
java·大数据·数据库
花间相见1 小时前
【计算基础|MySQL18】Buffer Pool 与内存管理
后端·sql·mysql
RacentCLM2 小时前
HTTPS只在登录时有用?关于TLS的误解讲清
数据库·网络协议·https
杨云龙UP2 小时前
生产实战:Keepalived + Nginx + MySQL 双向复制高可用架构部署与原理详解
运维·数据库·mysql·nginx·架构·keepalived·高可用架构
代钦塔拉2 小时前
【C++/Qt 进阶】设计模式系列(7):过滤器模式——把“筛选条件“变成可组合的积木
数据库·系统架构
TDengine (老段)2 小时前
TDengine IDMP 十大亮点功能(一)
大数据·数据库·物联网·时序数据库·tdengine·涛思数据
为思念酝酿的痛2 小时前
MySQL的基本查询
数据库·mysql
花间相见2 小时前
【计算基础|Redis01】【上手】安装连接、配置与通用命令
数据库·后端·面试