mysql主从复制原理
2020年12月20日
11:24
MySQL 是以容易学习和方便的高可用架构,被开发人员青睐的。而它的几乎所有的高可用架构,都直接依赖于 binlog
参考 <https://time.geekbang.org/column/article/76446>
1、主从复制原理:
一条更新语句的执行过程:
主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写 binlog
备库 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 读取中转日志,解析出日志里的命令,并执行。
2、物理日志 redo log 和逻辑日志 binlog。
binlog 的写入逻辑比较简单:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
执行器和 InnoDB 引擎在执行简单的 update 语句( update T set c=c+1 where ID=2;)时的内部流程:
(1)执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
(2)执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
(3)引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
(4)执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
(5)执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
分析一下在两阶段提交的不同时刻,MySQL 异常重启会出现什么现象:
(1)如果在图中时刻 A 的地方,也就是写入 redo log 处于 prepare 阶段之后、写 binlog 之前,发生了崩溃(crash),由于此时 binlog 还没写,redo log 也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog 还没写,所以也不会传到备库。
(2)在时刻 B,也就是 binlog 写完,redo log 还没 commit 前发生 crash,那崩溃恢复的时候 MySQL 会怎么处理?
- 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交;
- 如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog (通过xid查找)是否存在并完整:
a. 如果是,则提交事务;
b. 否则,回滚事务。
3、binlog格式
Show binary logs 查看所有binlog文件列表:
Show binlog event in 'mysql-bin.000005'
第一行 SET @@SESSION.GTID_NEXT='ANONYMOUS’你可以先忽略,后面文章我们会在介绍主备切换的时候再提到;
第二行是一个 BEGIN,跟第五行的 commit 对应,表示中间是一个事务;
第三行
Table_map event,用于说明接下来要操作的表是 test 库的表 t;
Update_rows event,用于定义修改的行为
最后一行是一个 COMMIT。你可以看到里面写着 xid=898726。
XID是用来联系bin log和redo log的。崩溃恢复的时候,比如redo log里面有一个事务是prepare状态,但是不知道是不是commit状态,那就可以用XID去bin log里面查询该事务到底有没有提交。有提交则是commit状态,若没有提交则回滚该事务。
借助mysqlbinlog工具查看binlog内容:
mysqlbinlog -vv var/lib/mysql/mysql-bin.000005 --start-position=29770832
从这个图中,我们可以看到以下几个信息:
(1)server id 1,表示这个事务是在 server_id=1 的这个库上执行的。
(2)每个 event 都有 CRC32 的值,这是因为我把参数 binlog_checksum 设置成了 CRC32。
在 MySQL 5.6.2 版本以后,还引入了 binlog-checksum 参数,用来验证 binlog 内容的正确性。对于 binlog 日志由于磁盘原因,可能会在日志中间出错的情况,MySQL 可以通过校验 checksum 的结果来发现。
(3)Table_map event 跟在图 5 中看到的相同,显示了接下来要打开的表,map 到数字 304。现在我们这条 SQL 语句只操作了一张表,如果要操作多张表呢?每个表都有一个对应的 Table_map event、都会 map 到一个单独的数字,用于区分对不同表的操作。
(4)我们在 mysqlbinlog 的命令中,使用了 -vv 参数是为了把内容都解析出来,所以从结果里面可以看到各个字段的值(比如,@1=4、 @2=4 这些值)。
(5)最后的 Xid event,用于表示事务被正确地提交了。
4、循环复制问题
如果节点 A 同时是节点 B 的备库,相当于又把节点 B 新生成的 binlog 拿过来执行了一次,然后节点 A 和 B 间,会不断地循环执行这个更新语句,也就是循环复制了。这个要怎么解决呢?
两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;
如果我们设置了双 M 结构,日志的执行流就会变成这样:
(1)从节点 A 更新的事务,binlog 里面记的都是 A 的 server id;
(2)传到节点 B 执行一次以后,节点 B 生成的 binlog 的 server id 也是 A 的 server id;
(3)再传回给节点 A,A 判断到这个 server id 与自己的相同,就不会再处理这个日志。所以,死循环在这里就断掉了
5、主备延迟问题(拓展)
在官方的 5.6 版本之前,MySQL 只支持单线程复制,由此在主库并发高、TPS 高时就会出现严重的主备延迟问题。
并行复制策略:
(1)按表分发策略
(2)按行分发策略
(3)按库分发策略(5.6版本)
(4)基于 WRITESET 的并行复制(5.7.22版本)