24 | MySQL是怎么保证主备一致的?
2019-01-07 林晓斌
在前面的文章中,我不止一次地和你提到了binlog,大家知道binlog可以用来归档,也可以用来
做主备同步,但它的内容是什么样的呢?为什么备库执行了binlog就可以跟主库保持一致了呢?
今天我就正式地和你介绍一下它。
毫不夸张地说,MySQL能够成为现下最流行的开源数据库,binlog功不可没。
在最开始,MySQL是以容易学习和方便的高可用架构,被开发人员青睐的。而它的几乎所有的
高可用架构,都直接依赖于binlog。虽然这些高可用架构已经呈现出越来越复杂的趋势,但都是
从最基本的一主一备演化过来的。
今天这篇文章我主要为你介绍主备的基本原理。理解了背后的设计原理,你也可以从业务开发的
角度,来借鉴这些设计思想。
MySQLMySQL主备的基本原理主备的基本原理
如图1所示就是基本的主备切换流程。
图 1 MySQL主备切换流程
在状态1中,客户端的读写都直接访问节点A,而节点B是A的备库,只是将A的更新都同步过
来,到本地执行。这样可以保持节点B和A的数据是相同的。
当需要切换的时候,就切成状态2。这时候客户端读写访问的都是节点B,而节点A是B的备库。
在状态1中,虽然节点B没有被直接访问,但是我依然建议你把节点B(也就是备库)设置成只读
(readonly)模式。这样做,有以下几个考虑:
1. 有时候一些运营类的查询语句会被放到备库上去查,设置为只读可以防止误操作;
2. 防止切换逻辑有bug,比如切换过程中出现双写,造成主备不一致;
3. 可以用readonly状态,来判断节点的角色。
你可能会问,我把备库设置成只读了,还怎么跟主库保持同步更新呢?
这个问题,你不用担心。因为readonly设置对超级(super)权限用户是无效的,而用于同步更新的
线程,就拥有超级权限。
接下来,我们再看看节点节点AA到到BB这条线的内部流程是什么样的这条线的内部流程是什么样的 。图2中画出的就是一个update
语句在节点A执行,然后同步到节点B的完整流程图。
图2 主备流程图
图2中,包含了我在上一篇文章中讲到的binlog和redo log的写入机制相关的内容,可以看到:主
库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写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读取中转日志,解析出日志里的命令,并执行。
这里需要说明,后来由于多线程复制方案的引入,sql_thread演化成为了多个线程,跟我们今天
要介绍的原理没有直接关系,暂且不展开。
分析完了这个长连接的逻辑,我们再来看一个问题:binlog里面到底是什么内容,为什么备库拿
过去可以直接执行。
binlogbinlog的三种格式对比的三种格式对比
我在第15篇答疑文章中,和你提到过binlog有两种格式,一种是statement,一种是row。可能你
在其他资料上还会看到有第三种格式,叫作mixed,其实它就是前两种格式的混合。
为了便于描述binlog的这三种格式间的区别,我创建了一个表,并初始化几行数据。
如果要在表中删除一行数据的话,我们来看看这个delete语句的binlog是怎么记录的。
注意,下面这个语句包含注释,如果你用MySQL客户端来做这个实验的话,要记得加-c参数,否
则客户端会自动去掉注释。
当binlog_format=statement时,binlog里面记录的就是SQL语句的原文。你可以用
mysql> 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;
mysql> show binlog events in 'master.000001';
命令看binlog中的内容。
图3 statement格式binlog 示例
现在,我们来看一下图3的输出结果。
第一行SET @@SESSION.GTID_NEXT='ANONYMOUS’你可以先忽略,后面文章我们会在
介绍主备切换的时候再提到;
第二行是一个BEGIN,跟第四行的commit对应,表示中间是一个事务;
第三行就是真实执行的语句了。可以看到,在真实执行的delete命令之前,还有一个“use
‘test’”命令。这条命令不是我们主动执行的,而是MySQL根据当前要操作的表所在的数据库,
自行添加的。这样做可以保证日志传到备库去执行的时候,不论当前的工作线程在哪个库
里,都能够正确地更新到test库的表t。
use 'test’命令之后的delete 语句,就是我们输入的SQL原文了。可以看到,binlog“忠实”地记
录了SQL命令,甚至连注释也一并记录了。
最后一行是一个COMMIT。你可以看到里面写着xid=61。你还记得这个XID是做什么用的吗?
如果记忆模糊了,可以再回顾一下第15篇文章中的相关内容。
为了说明statement 和 row格式的区别,我们来看一下这条delete命令的执行效果图:
图4 delete执行warnings
可以看到,运行这条delete命令产生了一个warning,原因是当前binlog设置的是statement格
式,并且语句中有limit,所以这个命令可能是unsafe的。
为什么这么说呢?这是因为delete 带limit,很可能会出现主备数据不一致的情况。比如上面这个
例子:
1. 如果delete语句使用的是索引a,那么会根据索引a找到第一个满足条件的行,也就是说删除
的是a=4这一行;
2. 但如果使用的是索引t_modified,那么删除的就是 t_modified='2018-11-09’也就是a=5这一
行。