在前面的文章中,我不止一次地和你提到了binlog,你们知道binlog能够用来归档,也能够用来作主备同步,但它的内容是什么样的呢?为何备库执行了binlog就能够跟主库保持一致了呢?今天我就正式地和你介绍一下它。mysql
绝不夸张地说,MySQL可以成为现下最流行的开源数据库,binlog功不可没。sql
在最开始,MySQL是以容易学习和方便的高可用架构,被开发人员青睐的。而它的几乎全部的高可用架构,都直接依赖于binlog。虽然这些高可用架构已经呈现出愈来愈复杂的趋势,但都是从最基本的一主一备演化过来的。数据库
今天这篇文章我主要为你介绍主备的基本原理。理解了背后的设计原理,你也能够从业务开发的角度,来借鉴这些设计思想。多线程
如图1所示就是基本的主备切换流程。架构
在状态1中,客户端的读写都直接访问节点A,而节点B是A的备库,只是将A的更新都同步过来,到本地执行。这样能够保持节点B和A的数据是相同的。函数
当须要切换的时候,就切成状态2。这时候客户端读写访问的都是节点B,而节点A是B的备库。工具
在状态1中,虽然节点B没有被直接访问,可是我依然建议你把节点B(也就是备库)设置成只读(readonly)模式。这样作,有如下几个考虑:学习
有时候一些运营类的查询语句会被放到备库上去查,设置为只读能够防止误操做;spa
防止切换逻辑有bug,好比切换过程当中出现双写,形成主备不一致;线程
能够用readonly状态,来判断节点的角色。
你可能会问,我把备库设置成只读了,还怎么跟主库保持同步更新呢?
这个问题,你不用担忧。由于readonly设置对超级(super)权限用户是无效的,而用于同步更新的线程,就拥有超级权限。
接下来,咱们再看看节点A到B这条线的内部流程是什么样的。图2中画出的就是一个update语句在节点A执行,而后同步到节点B的完整流程图。
图2中,包含了我在上一篇文章中讲到的binlog和redo log的写入机制相关的内容,能够看到:主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写binlog。
备库B跟主库A之间维持了一个长链接。主库A内部有一个线程,专门用于服务备库B的这个长链接。一个事务日志同步的完整过程是这样的:
在备库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读取中转日志,解析出日志里的命令,并执行。
这里须要说明,后来因为多线程复制方案的引入,sql_thread演化成为了多个线程,跟咱们今天要介绍的原理没有直接关系,暂且不展开。
分析完了这个长链接的逻辑,咱们再来看一个问题:binlog里面究竟是什么内容,为何备库拿过去能够直接执行。
我在第15篇答疑文章中,和你提到过binlog有两种格式,一种是statement,一种是row。可能你在其余资料上还会看到有第三种格式,叫做mixed,其实它就是前两种格式的混合。
为了便于描述binlog的这三种格式间的区别,我建立了一个表,并初始化几行数据。
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');
若是要在表中删除一行数据的话,咱们来看看这个delete语句的binlog是怎么记录的。
注意,下面这个语句包含注释,若是你用MySQL客户端来作这个实验的话,要记得加-c参数,不然客户端会自动去掉注释。
mysql> delete from t /*comment*/ where a>=4 and t_modified<='2018-11-10' limit 1;
当binlog_format=statement时,binlog里面记录的就是SQL语句的原文。你能够用
mysql> show binlog events in 'master.000001';
命令看binlog中的内容。
如今,咱们来看一下图3的输出结果。
为了说明statement 和 row格式的区别,咱们来看一下这条delete命令的执行效果图:
能够看到,运行这条delete命令产生了一个warning,缘由是当前binlog设置的是statement格式,而且语句中有limit,因此这个命令多是unsafe的。
为何这么说呢?这是由于delete 带limit,极可能会出现主备数据不一致的状况。好比上面这个例子:
若是delete语句使用的是索引a,那么会根据索引a找到第一个知足条件的行,也就是说删除的是a=4这一行;
但若是使用的是索引t_modified,那么删除的就是 t_modified='2018-11-09’也就是a=5这一行。
因为statement格式下,记录到binlog里的是语句原文,所以可能会出现这样一种状况:在主库执行这条SQL语句的时候,用的是索引a;而在备库执行这条SQL语句的时候,却使用了索引t_modified。所以,MySQL认为这样写是有风险的。
那么,若是我把binlog的格式改成binlog_format=‘row’, 是否是就没有这个问题了呢?咱们先来看看这时候binog中的内容吧。
能够看到,与statement格式的binlog相比,先后的BEGIN和COMMIT是同样的。可是,row格式的binlog里没有了SQL语句的原文,而是替换成了两个event:Table_map和Delete_rows。
Table_map event,用于说明接下来要操做的表是test库的表t;
Delete_rows event,用于定义删除的行为。
其实,咱们经过图5是看不到详细信息的,还须要借助mysqlbinlog工具,用下面这个命令解析和查看binlog中的内容。由于图5中的信息显示,这个事务的binlog是从8900这个位置开始的,因此能够用start-position参数来指定从这个位置的日志开始解析。
mysqlbinlog -vv data/master.000001 --start-position=8900;
从这个图中,咱们能够看到如下几个信息:
你能够看到,当binlog_format使用row格式的时候,binlog里面记录了真实删除行的主键id,这样binlog传到备库去的时候,就确定会删除id=4的行,不会有主备删除不一样行的问题。
基于上面的信息,咱们来讨论一个问题:为何会有mixed这种binlog格式的存在场景?推论过程是这样的:
也就是说,mixed格式能够利用statment格式的优势,同时又避免了数据不一致的风险。
所以,若是你的线上MySQL设置的binlog格式是statement的话,那基本上就能够认为这是一个不合理的设置。你至少应该把binlog的格式设置为mixed。
好比咱们这个例子,设置为mixed后,就会记录为row格式;而若是执行的语句去掉limit 1,就会记录为statement格式。
固然我要说的是,如今愈来愈多的场景要求把MySQL的binlog格式设置成row。这么作的理由有不少,我来给你举一个能够直接看出来的好处:恢复数据。
接下来,咱们就分别从delete、insert和update这三种SQL语句的角度,来看看数据恢复的问题。
经过图6你能够看出来,即便我执行的是delete语句,row格式的binlog也会把被删掉的行的整行信息保存起来。因此,若是你在执行完一条delete语句之后,发现删错数据了,能够直接把binlog中记录的delete语句转成insert,把被错删的数据插入回去就能够恢复了。
若是你是执行错了insert语句呢?那就更直接了。row格式下,insert语句的binlog里会记录全部的字段信息,这些信息能够用来精肯定位刚刚被插入的那一行。这时,你直接把insert语句转成delete语句,删除掉这被误插入的一行数据就能够了。
若是执行的是update语句的话,binlog里面会记录修改前整行的数据和修改后的整行数据。因此,若是你误执行了update语句的话,只须要把这个event先后的两行信息对调一下,再去数据库里面执行,就能恢复这个更新操做了。
其实,由delete、insert或者update语句致使的数据操做错误,须要恢复到操做以前状态的状况,也时有发生。MariaDB的Flashback工具就是基于上面介绍的原理来回滚数据的。
虽然mixed格式的binlog如今已经用得很少了,但这里我仍是要再借用一下mixed格式来讲明一个问题,来看一下这条SQL语句:
mysql> insert into t values(10,10, now());
若是咱们把binlog格式设置为mixed,你以为MySQL会把它记录为row格式仍是statement格式呢?
先不要着急说结果,咱们一块儿来看一下这条语句执行的效果。
能够看到,MySQL用的竟然是statement格式。你必定会奇怪,若是这个binlog过了1分钟才传给备库的话,那主备的数据不就不一致了吗?
接下来,咱们再用mysqlbinlog工具来看看:
从图中的结果能够看到,原来binlog在记录event的时候,多记了一条命令:SET TIMESTAMP=1546103491。它用 SET TIMESTAMP命令约定了接下来的now()函数的返回时间。
所以,不论这个binlog是1分钟以后被备库执行,仍是3天后用来恢复这个库的备份,这个insert语句插入的行,值都是固定的。也就是说,经过这条SET TIMESTAMP命令,MySQL就确保了主备数据的一致性。
我以前看过有人在重放binlog数据的时候,是这么作的:用mysqlbinlog解析出日志,而后把里面的statement语句直接拷贝出来执行。
你如今知道了,这个方法是有风险的。由于有些语句的执行结果是依赖于上下文命令的,直接执行的结果极可能是错误的。
因此,用binlog来恢复数据的标准作法是,用 mysqlbinlog工具解析出来,而后把解析结果整个发给MySQL执行。相似下面的命令:
mysqlbinlog master.000001 --start-position=2738 --stop-position=2973 | mysql -h127.0.0.1 -P13000 -u$user -p$pwd;
这个命令的意思是,将 master.000001 文件里面从第2738字节到第2973字节中间这段内容解析出来,放到MySQL去执行。
经过上面对MySQL中binlog基本内容的理解,你如今能够知道,binlog的特性确保了在备库执行相同的binlog,能够获得与主库相同的状态。
所以,咱们能够认为正常状况下主备的数据是一致的。也就是说,图1中A、B两个节点的内容是一致的。其实,图1中我画的是M-S结构,但实际生产上使用比较多的是双M结构,也就是图9所示的主备切换流程。
对比图9和图1,你能够发现,双M结构和M-S结构,其实区别只是多了一条线,即:节点A和B之间老是互为主备关系。这样在切换的时候就不用再修改主备关系。
可是,双M结构还有一个问题须要解决。
业务逻辑在节点A上更新了一条语句,而后再把生成的binlog 发给节点B,节点B执行完这条更新语句后也会生成binlog。(我建议你把参数log_slave_updates设置为on,表示备库执行relay log后生成binlog)。
那么,若是节点A同时是节点B的备库,至关于又把节点B新生成的binlog拿过来执行了一次,而后节点A和B间,会不断地循环执行这个更新语句,也就是循环复制了。这个要怎么解决呢?
从上面的图6中能够看到,MySQL在binlog中记录了这个命令第一次执行时所在实例的server id。所以,咱们能够用下面的逻辑,来解决两个节点间的循环复制的问题:
规定两个库的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与本身的相同,就不会再处理这个日志。因此,死循环在这里就断掉了。
今天这篇文章,我给你介绍了MySQL binlog的格式和一些基本机制,是后面我要介绍的读写分离等系列文章的背景知识,但愿你能够认真消化理解。
binlog在MySQL的各类高可用方案上扮演了重要角色。今天介绍的能够说是全部MySQL高可用方案的基础。在这之上演化出了诸如多节点、半同步、MySQL group replication等相对复杂的方案。
我也跟你介绍了MySQL不一样格式binlog的优缺点,和设计者的思考。但愿你在作系统开发时候,也能借鉴这些设计思想。
最后,我给你留下一个思考题吧。
说到循环复制问题的时候,咱们说MySQL经过判断server id的方式,断掉死循环。可是,这个机制其实并不完备,在某些场景下,仍是有可能出现死循环。
你能构造出一个这样的场景吗?又应该怎么解决呢?
你能够把你的设计和分析写在评论区,我会在下一篇文章跟你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一块儿阅读。
上期我留给你的问题是,你在何时会把线上生产库设置成“非双1”。我目前知道的场景,有如下这些:
业务高峰期。通常若是有预知的高峰期,DBA会有预案,把主库设置成“非双1”。
备库延迟,为了让备库尽快遇上主库。@永恒记忆和@Second Sight提到了这个场景。
用备份恢复主库的副本,应用binlog的过程,这个跟上一种场景相似。
批量导入数据的时候。
通常状况下,把生产库改为“非双1”配置,是设置innodb_flush_logs_at_trx_commit=二、sync_binlog=1000。