MySQL是怎么保证主备一致的?
MySQL 主备的基本原理
- 基本的主备切换流程
- 状态 1:客户端的读写都直接访问节点 A,而节点 B 是 A 的备库
- 状态 2:切换时,读写访问的都是节点 B,而节点 A 是 B 的备库
- 注意:建议备库只设置制度(readonly)模式
- 虽然是只读,但是因为 readonly 设置对超级 (super) 权限用户是无效的,而用于同步更新的线程,就拥有超级权限
- 节点 A 到节点 B 的内部流程
- 主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写 binlog
- 备库 B 跟主库 A 之间维持了一个长连接
- 完整流程
- 备库 B 上通过
change master
命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 - 在备库 B 上执行
start slave
命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接 - 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B
- 备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)
- sql_thread 读取中转日志,解析出日志里的命令,并执行
- 备库 B 上通过
binlog 的三种格式对比
- 目前有三种格式:statement、row、mixed
- 要注意修改 binlog 格式为 statement,可以用过 show variables like ‘%binlog_format%’; 查看
- sql
# 使用后重启 mysql
set global binlog_format='STATEMENT'
- docker
- 首先配置一下 my.cnf
[mysqld] server_id=1000 binlog-ignore-db=mysql log-bin=mall-mysql-bin binlog_cache_size=1M binlog_format=statement expire_logs_days=7 slave_skip_errors=1062
- 运行 docker
docker run -p 3305:3306 --name mysql-master --restart=always --privileged=true \ -v /root/mysql-master/log:/var/log/mysql \ -v /root/mysql-master/data:/var/lib/mysql \ -v /root/mysql-master/conf:/etc/mysql \ -v /etc/localtime:/etc/localtime:ro \ -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7.30
- 首先配置一下 my.cnf
- 首先创建一个数据表
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');
4. 执行一次删除语句,查看 delete 语句在 binlog 是怎么记录的
- 要注意执行前,如果是通过 mysql 客户端 启动的,要执行 mysql -c -root ***,否则下面的注释行不会记录在 binlog
delete from t /*comment*/ where a>=4 and t_modified<='2018-11-10' limit 1;
- 当
binlog_format=statement
时,binlog 里面记录的就是 SQL 语句的原文
- 首先要通过 show variables like ‘log_%’; 查看 log_bin 参数是否为 ON,否则是看不到日志
- 通过 show binary logs; 去查看有哪些 binlog 日志,一般最大的 File_size 会记录刚刚执行的 sql
show binlog events in 'mall-mysql-bin.000004';
- 输出结果解释
- 第一行:可先忽略
- 第二、四行: BEGIN 与 COMMIT 对应,中间是事务
- 第三行:真实执行语句
- 其实,刚刚执行的 delete 语句,注意:在 statement 格式下,是 unsafe 的,因为会出现主从不一致的情况
- 不一致的例子
- 如果 delete 语句使用的是索引 a,那么会根据索引 a 找到第一个满足条件的行,也就是说删除的是 a=4 这一行
- 但如果使用的是索引 t_modified,那么删除的就是 t_modified='2018-11-09’ 也就是 a=5 这一行
- 修改
binlog_format='row'
,再看看 binlog 实际内容
- 这里的 binlog 里没有了 SQL 语句的原文,而是换成两个 event
- Table_map event: 要操作的表是 test 库的表 t
- Delete_rows event:用于定义删除的行为
- 实际上面的信息还是没看到i昂西信息,需要借助 mysqlbinlog 工具
- 如果没有的话,执行 yum install mysql,会有相关的工具下载下来
# 其中 2191 是上面从对应的位置开始的
# 我这边没执行成功,因为可能 8 的版本,5 的 mysqlbinlog 没办法解析
mysqlbinlog -vv binlog.000058 --start-position=2191
- 其中的信息如下:
- server id 1:表示这个事务是在 server_id=1 的这个库上执行的
- CRC32:每个 event 都有 CRC32 的值,主要是 binlog_checksum 设置为 CRC32
- Table_map event:实际 map 到数字应该是 93。如果有操作多个表,每个表会有对应的数字
- @1=4、 @2=4…:实际就是对应删除的行每一列的值
- binlog_row_image 默认配置为 FULL,所以 Delete_event 包含了删除行的所有字段的值,如果把 binlog_row_image 设置为 MINIMAL,则只会记录必要的信息,在这个例子里,就是只会记录 id=4 这个信息
- 最后的 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 中
- MySQL 就取了个折中方案,也就是有了 mixed 格式的 binlog。MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式
- 越来越多的场景要求把 MySQL 的 binlog 格式设置成 row,最直接好处:恢复数据
- delete 语句:
- row 格式的 binlog 保留了被删掉的行的整行信息。可以将 delete 语句转换成 insert 数据插入回去恢复
- insert 语句
- row 格式下,insert 语句的 binlog 里会记录所有的字段信息。可以将 insert 转成 delete 语句,删除误插入的一行数据
- update 语句:
- binlog 里面会记录修改前整行的数据和修改后的整行数据。只需要把 event 的前后两行信息对调一下,就可以去数据库里面执行恢复更新操作
- mixed 格式的 binlog 现在已经用得不多了
- 关于时间戳的问题
- 首先把 binlog 格式设置为 mixed,然后执行下面语句
insert into t values(10,10, now());
- MySQL 会选择使用 statement 格式。如果 binlog 过了 1 分钟才传给备库的话,主备的数据不会造成不一致,原因为
- 当使用 mysqlbinlog 工具查看的时候,它会多一条 SET TIMESTAMP 命令
- 总结
- binlog 来恢复数据的标准做法是,用 mysqlbinlog 工具解析出来,然后把解析结果整个发给 MySQL 执行。类似下面的命令
- 命令意思:将 master.000001 文件里面从第 2738 字节到第 2973 字节中间这段内容解析出来,放到 MySQL 去执行
mysqlbinlog master.000001 --start-position=2738 --stop-position=2973 | mysql -h127.0.0.1 -P13000 -u$user -p$pwd;
循环复制问题(双 M 结构)
- 业务逻辑
- 节点 A 上更新了一条语句,然后再把生成的 binlog 发给节点 B,节点 B 执行完这条更新语句后也会生成 binlog
- 建议:log_slave_updates 设置为 on,表示备库执行 relay log 后生成 binlog。可以让更新事件在备库上也记录一份
- 上面业务逻辑可能会出现循环复制问题,解决的方式
- 规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系
- 一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog
- 每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志