以下内容出自《MySQL 实战 45 讲》 https://time.geekbang.org/column/article/76446
24 | MySQL是怎么保证主备一致的?
MySQL 主备的基本原理
如图所示就是基本的主备切换流程。(M-S结构)
节点 A 到 B 这条线的内部流程是什么样的 ?
下图中画出的就是一个 update 语句在节点 A 执行,然后同步到节点 B 的完整流程图。
备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。一个事务日志同步的完整过程是这样的:
1、在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
2、在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。
3、主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
4、备库 B 拿到 binlog 后,写到本地文件,称为 中转日志(relay log)。
5、sql_thread 读取中转日志,解析出日志里的命令,并执行。(后续 sql_thread 演化成为了多个线程)
binlog 的三种格式对比
Statement(Statement-Based Replication,SBR):每一条会修改数据的 SQL 都会记录在 binlog 中。
Row(Row-Based Replication,RBR):不记录 SQL 语句上下文信息,仅保存哪条记录被修改。
Mixed(Mixed-Based Replication,MBR):Statement 和 Row 的混合体。
log_bin = on 时,代表 binlog 开启。
show variables like 'log_bin'
常见 binlog 操作
修改 binglog_format
- 当前会话生效:
set binlog_format = 'MIXED';
- 全局生效,但 MySQL 重启失效:
set global binlog_format = 'MIXED';
- 一劳永李修改生效:修改 mysql 的配置文件, 在
mysqld
下加配置binlog_format=MIXED
查看所有 binlog 日志
show binary logs;
show master logs;
查看 master 状态
show master status ;
刷新 binlog
正常来说,一个 binlog 写满之后,会自动切换到下一个 binlog 开始写,不过我们也可以执行一个 flush logs 命令来手动刷新 binlog,手动刷新 binlog 之后,就会产生一个新的 binlog 日志文件,接下来所有的 binlog 日志都将记录到新的文件中。
重置 binlog
reset master 可以重置 binlog 日志文件,让日志重新从 000001 开始记录,不过如果当前主机有一个或者多个从机在运行,那么该命令就运行不了(因为从机是通过 binlog 来实现数据库同步的,主机把 binlog 清空了,从机会报找不到 binlog 的错误)。
查看 binlog
mysqlbinlog 命令
mysqlbinlog命令使用时报错 unknown variable ‘default-character-set=utf8mb4’ 的解决办法_aben_sky的博客-CSDN博客
show binlog events
show binlog events : 只看第一个 binlog 文件内容
show binlog events in ‘binlog_name’: 查看指定 binlog 文件的内容
三种格式说明对比
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `a` (`a`),
KEY `t_modified`(`t_modified`)
) ENGINE=InnoDB;
insert into t values(1,1,'2018-11-13');
insert into t values(2,2,'2018-11-12');
insert into t values(3,3,'2018-11-11');
insert into t values(4,4,'2018-11-10');
insert into t values(5,5,'2018-11-09');
执行下面的删除语句
mysql> delete from t /*comment*/ where a>=4 and t_modified<='2018-11-10' limit 1;
当 binlog_format = statement
show master status ;
show binlog events in 'binlog.000004';
- 第一行 SET @@SESSION.GTID_NEXT='ANONYMOUS’你可以先忽略,后面文章我们会在介绍主备切换的时候再提到;
- 第二行是一个 BEGIN,跟第四行的 commit 对应,表示中间是一个事务;
- 第三行就是真实执行的语句了。可以看到,在真实执行的 delete 命令之前,还有一个“use ‘test’”命令。这条命令不是我们主动执行的,而是 MySQL 根据当前要操作的表所在的数据库,自行添加的。这样做可以保证日志传到备库去执行的时候,不论当前的工作线程在哪个库里,都能够正确地更新到 test 库的表 t。use 'test’命令之后的 delete 语句,就是我们输入的 SQL 原文了。可以看到,binlog“忠实”地记录了 SQL 命令,甚至连注释也一并记录了。
- 最后一行是一个 COMMIT。你可以看到里面写着 xid=2791。
XID 是用来联系 bin log 和 redo log 的。比如 redo log 里面有一个事务是 prepare 状态,但是不知道是不是 commit 状态,那就可以用 XID 去 bin log 里面查询该事务到底有没有提交。有提交则是 commit 状态,若没有提交则回滚该事务。
同时,执行命令 show warnings
命令。可能会看到下面的返回;
原因:当前 binlog 设置的是 statement 格式,并且语句中有 limit,所以这个命令可能是 unsafe 的。
可能会出现主备数据不一致的情况。
由于 statement 格式下,记录到 binlog 里的是语句原文,因此可能会出现这样一种情况:在主库执行这条 SQL 语句的时候,用的是索引 a;而在备库执行这条 SQL 语句的时候,却使用了索引 t_modified。因此,MySQL 认为这样写是有风险的。
当 binlog_foramt = row
可以看到,与 statement 格式的 binlog 相比,前后的 BEGIN 和 COMMIT 是一样的。但是,row 格式的 binlog 里没有了 SQL 语句的原文,而是替换成了两个 event:Table_map 和 Delete_rows。
- Table_map event,用于说明接下来要操作的表是 test 库的表 t;
- Delete_rows event,用于定义删除的行为。
用 mysqlbinlog 查看和解析 binlog 的内容,上图中显示,事务是从 2885 这个位置开启的,用 start-position 参数来指定从这个位置的日志开始解析。
- server id 1,表示这个事务是在 server_id=1 的这个库上执行的。
- 每个 event 都有 CRC32 的值,参数 binlog_checksum,用于校验 binlog 的完整性。
- Table_map:
test
.t
mapped to number 162: 显示了接下来要打开的表,map 到数字 162。 - 参数 binlog_row_image 的默认配置是 FULL, 因此 Delete event 包含了删掉的行的所有字段的值。如果把 binlog_row_image 设置为 MINIMAL,则只会记录必要的信息,在这个例子里,就是只会记录 id=5 这个信息。
- 最后的 Xid event,用于表示事务被正确地提交了。
当 binlog_format 使用 row 格式的时候,binlog 里面记录了真实删除行的主键 id,这样 binlog 传到备库去的时候,就肯定会删除 id=4 的行,不会有主备删除不同行的问题。
为什么会有 mixed 格式的 binlog?
推论:
- 有些 statement 格式的 binlog 可能会导致主备不一致,所以要使用 row 格式。
- row 格式的缺点是,很占空间。比如你用一个 delete 语句删掉 10 万行数据,用 statement 的话就是一个 SQL 语句被记录到 binlog 中,占用几十个字节的空间。但如果用 row 格式的 binlog,就要把这 10 万条记录都写到 binlog 中。这样做,不仅会占用更大的空间,同时写 binlog 也要耗费 IO 资源,影响执行速度。
- MySQL 就取了个折中方案,也就是有了 mixed 格式的 binlog。mixed 格式的意思是,MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。
但是,mixed 格式的 binlog 用的不多,大多数场景中,还是设置为 row,好处就是 恢复数据。
循环复制问题
实际生产中,使用比较多的是双 M 结构。
节点 A 和 B 之间总是互为主备关系。这样在切换的时候就不用再修改主备关系。
双 M 结构的问题:在节点 A 上更新了一条语句,再把 binlog 发给 B,节点 B 执行后也会生成 binlog,如果节点 A 同时是节点 B 的备库,那么 A B 之间,会不断循环执行,也就是循环复制。
可以用下面的逻辑,来解决两个节点间的循环复制的问题:
-
规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;
-
一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog;
-
每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。
按照这个逻辑,如果我们设置了双 M 结构,日志的执行流就会变成这样:
- 从节点 A 更新的事务,binlog 里面记的都是 A 的 server id;
- 传到节点 B 执行一次以后,节点 B 生成的 binlog 的 server id 也是 A 的 server id;
- 再传回给节点 A,A 判断到这个 server id 与自己的相同,就不会再处理这个日志。所以,死循环在这里就断掉了。