专注于Java领域优质技术,欢迎关注作者:田守枝 田守枝的技术部落格
本文深入介绍Mysql Binlog的应用场景,以及如何与MQ、elasticsearch、redis等元件的保持资料最终一致。最后通过案例深入分析binlog中几乎所有event是如何产生的,作用是什么。
1 基于binlog的主从复制
Mysql 5.0以后,支援通过binary log(二进位制日志)以支援主从复制。复制允许将来自一个MySQL数据库服务器(master) 的资料复制到一个或多个其他MySQL数据库服务器(slave),以实现灾难恢复、水平扩充套件、统计分析、远端资料分发等功能。二进位制日志中储存的内容称之为事件,每一个数据库更新操作(Insert、Update、Delete,不包括Select)等都对应一个事件。
注意:本文不是讲解mysql主从复制,而是讲解binlog的应用场景,binlog中包含哪些型别的event,这些event的作用是什么。你可以理解为,是对主从复制中关于binlog解析的细节进行深度剖析。而讲解主从复制主要是为了理解binlog的工作流程。
下面以mysql主从复制为例,讲解一个从库是如何从主库拉取binlog,并回放其中的event的完整流程。mysql主从复制的流程如下图所示:
主要分为3个步骤:
第一步:master在每次准备提交事务完成资料更新前,将改变记录到二进位制日志(binary log)中(这些记录叫做二进位制日志事件,binary log event,简称event)第二步:slave启动一个I/O执行绪来读取主库上binary log中的事件,并记录到slave自己的中继日志(relay log)中。 第三步:slave还会起动一个SQL执行绪,该执行绪从relay log中读取事件并在备库执行,从而实现备库资料的更新。2 binlog的应用场景
binlog本身就像一个螺丝刀,它能发挥什么样的作用,完全取决你怎么使用。就像你可以使用螺丝刀来修电器,也可以用其来固定家俱。
2.1 读写分离
最典型的场景就是通过Mysql主从之间通过binlog复制来实现横向扩充套件,来实现读写分离。如下图所示:
在这种场景下:
有一个主库Master,所有的更新操作都在master上进行同时会有多个Slave,每个Slave都连线到Master上,获取binlog在本地回放,实现资料复制。在应用层面,需要对执行的sql进行判断。所有的更新操作都通过Master(Insert、Update、Delete等),而查询操作(Select等)都在Slave上进行。由于存在多个slave,所以我们可以在slave之间做负载均衡。通常业务都会借助一些数据库中介软件,如tddl、sharding-jdbc等来完成读写分离功能。因为工作性质的原因,笔者见过最多的一个业务,一个master,后面挂了20多个slave。笔者之前写过一篇关于数据库中介软件实现原理的文章,感兴趣的读者可以参考:数据库中介软件详解
2.2 资料恢复
一些同学可能有误删除数据库记录的经历,或者因为误操作导致数据库存在大量脏资料的情况。例如笔者,曾经因为误操作污染了业务方几十万资料记录。
如何将脏资料恢复成原来的样子?如果恢复已经被删除的记录?
这些都可以通过反解binlog来完成,笔者也是通过这个手段,来恢复业务方的记录。
2.3 资料最终一致性
在实际开发中,我们经常会遇到一些需求,在数据库操作成功后,需要进行一些其他操作,如:传送一条讯息到MQ中、更新快取或者更新搜索引擎中的索引等。
如何保证数据库操作与这些行为的一致性,就成为一个难题。以数据库与redis快取的一致性为例:操作数据库成功了,可能会更新redis失败;反之亦然。很难保证二者的完全一致。
遇到这种看似无解的问题,最好的办法是换一种思路去解决它:不要同时去更新数据库和其他元件,只是简单的更新数据库即可。
如果数据库操作成功,必然会产生binlog。之后,我们通过一个元件,来模拟的mysql的slave,拉取并解析binlog中的资讯。通过解析binlog的资讯,去异步的更新快取、索引或者传送MQ讯息,保证数据库与其他元件中资料的最终一致。
在这里,我们将模拟slave的元件,统一称之为binlog同步元件。你并不需要自己编写这样的一个元件,已经有很多开源的实现,例如linkedin的databus,阿里巴巴的canal,美团点评的puma等。
当我们通过binlog同步元件完成资料一致性时,此时架构可能如下图所示:
增量索引
通常索引分为全量索引和增量索引。对于增量索引的部分,可以通过监听binlog变化,根据binlog中包含的资讯,转换成es语法,进行实时索引更新。当然,你可能并没有使用es,而是solr,这里只是以es举例。
可靠讯息
可靠讯息是指的是:保证本地事务与传送讯息到MQ行为的一致性。一些业务使用本地事务表或者独立讯息服务,来保证二者的最终一致。Apache RocketMQ在4.3版本开源了事务讯息,也是用于完成此功能。事实上,这两种方案,都有一定侵入性,对业务不透明。通过订阅binlog来发送可靠讯息,则是一种解耦、无侵入的方案。关于可靠讯息,笔者最近写了一篇文章, 感兴趣的读者可以参考:可靠讯息一致性的奇淫技巧。
快取一致性
业务经常遇到的一个问题是,如何保证数据库中记录和快取中资料的一致性。不妨换一种思路,只更新数据库,数据库更新成功后,通过拉取binlog来异步的更新快取(通常是删除,让业务回源到数据库)。如果数据库更新失败,没有对应binlog,那么也不会去更新快取,从而实现最终一致性。
可以看到,binlog是一把利器,可以保证数据库与与其他任何元件(es、mq、redis等)的最终一致。这是一种优雅的、通用的、无业务入侵的、彻底的解决方案。我们没有必要再单独的研究某一种其他元件如何与数据库保持最终一致,可以通过binlog来实现统一的解决方案。
在实际开发中,你可以简单的像上图那样,每个应用场景都模拟一个slave,各自连线到Mysql上去拉取binlog,master会给每个连线上来的slave一份完整的binlog拷贝,业务拿到各自的binlog之后进行消费,彼此之间互不影响。但是这样,有一些弊端,多个slave会给master带来一些额外管理上的开销,网络卡流量也将翻倍的增长。
我们可以进行一些优化,之所以不同场景模拟多个slave来连线master获取同一份binlog,本质上要满足的是:一份binlog资料,同时提供给多个不同业务场景使用,彼此之间互不影响。
显然,讯息中介软件是一个很好的解决方案。现在很多主流的讯息中介软件,都支援consumer group的概念,如kafka、rocketmq等。同一个topic中的资料,可以由多个不同consumer group来消费,且不同的consumer group之间是相互隔离的,例如:当前消费到的位置(offset)。
因此,我们完全可以将binlog,统一都发送到MQ中,不同的应用场景使用不同的consumer group来消费,彼此之间互不影响。此时架构如下图所示:
通过这样方式,我们巧妙的达到了一份资料多个应用场景来使用。一般,一个Mysql例项中可能会建立多个库(Database),通常我们会将一个库的binlog放到一个对应的MQ中的Topic中。
当将binlog传送到MQ中后,我们就可以利用MQ的一些高阶特性了。例如binlog传送到MQ过快,消费方来不及消费,可以利用MQ的讯息堆积能力进行流量削峰。还可以利用MQ的讯息回溯功能,例如一个业务需要消费历史的binlog,此时MQ中如果还有储存,那么就可以直接进行回溯。
当然,有一些binlog同步元件可能实现了类似于MQ的功能,此时你就无序再单独的使用MQ。
2.4 异地多活
一个更大的应用场景,异地多活场景下,跨资料中心之间的资料同步。这种场景的下,多个数据中心都需要写入资料,并且往对方同步。以下是一个简化的示意图:
这里有一些特殊的问题需要处理。典型的包括:
资料冲突:双方同时插入了一个相同主键的值,那么往对方同步时,就会出现主键冲突的错误。资料回环:一个库A中插入的资料,通过binlog同步到另外一个库B中,依然会产生binlog。此时库B的资料再次同步回库A,如此反复,就形成了一个死循环。如何解决资料冲突、资料回环,就变成了binlog同步元件要解决的问题。同样,业界也有了成熟的实现,比较知名的有阿里开源的otter,以及摩拜(已经属于美团)的DRC等。
笔者之前写过一篇文章,介绍如何在多机房进行资料同步,感兴趣的读者可以参考以下文章:异地多活场景下的资料同步之道
2.5 小结
如前所属,binlog的作用如此强大。因此,你可能想知道binlog档案中到底包含了哪些内容,为什么具有如此的魔力?在进行一些数据库操作时,例如:Insert、Update、Delete等,到底会对binlog产生什么样的影响?这正是本文要下来要讲解的内容。
3 Binlog事件详解
Mysql已经经历了多个版本的释出,最新已经到8.x,然而目前企业中主流使用的还是Mysql 5.6或5.7。不同版本的Mysql中,binlog的格式和事件型别可能会有些细微的变化,不过暂时我们并不讨论这些细节。总的来说,binlog档案中储存的内容称之为二进位制事件,简称事件。我们的每一个数据库更新操作(Insert、Update、Delete等),都会对应的一个事件。
从大的方面来说,binlog主要分为2种格式:
Statement模式:binlog中记录的就是我们执行的SQL;Row模式:binlog记录的是每一行记录的每个字段变化前后得到值。熟悉主从复制的同学,应该知道,还有第三种模式Mixed(即混合模式),从严格意义上来说,这并不是一种新的binlog格式,只是结合了Statement和Row两种模式而已。
当我们选择不同的binlog模式时,在binlog档案包含的事件型别也不相同,如: 1)在Statement模式下,我们就看不到Row模式下独有的事件型别。2)有一些型别的event,必须在我们开启某些特定配置的情况下,才会出现;3)当然也会有一些公共的event型别,在任何模式下都会出现。
Mysql中定义了30多个event型别,这里并不打算将所有的事件型别提前列出,这样没有意义,只会让读者茫然不知所措。笔者将会在必要的地方,介绍遇到的每一种event型别的作用。
目前我们先从宏观的角度对binlog有一个感性的认知。
3.1 多档案储存
mysql 将数据库更新操作对应的event记录到本地的binlog档案中,显然在一个档案中记录所有的event是不可能的,过大的档案会给我们的运维带来麻烦,如删除一个大档案,在I/O排程方面会给我们带来不可忽视的资源开销。
因此,目前基本上所有支援本地档案储存的元件,如MQ、Mysql等,都会控制一个档案的大小。在资料量较多的情况下,就分配到多个档案进行储存。
在mysql中,我们可以通过"show binary logs"语句,来检视当前有多少个binlog档案,以及每个binlog档案的大小,如下:
另外,mysql提供了:
max_binlog_size配置项,用于控制一个binlog档案的大小,预设是1Gexpire_logs_days配置项,可以控制binlog档案保留天数,预设是0,也就是永久保留。在实际生产环境中,一般无法保留所有的历史binlog。因为一条记录可能会变更多次,记录依然是一条,但是对应的binlog事件就会有多个。在资料变更比较频繁的情况下,就会产生大量的binlog档案。此时,则无法保留所有的历史binlog档案。
在mysql的percona分支上,还提供了max_binlog_files配置项,用于设定可以保留的binlog档案数量,以便我们更精确的控制binlog档案占用的磁盘空间。这是一个非常有用的配置,笔者曾经遇到一个库,大约10分钟就会产生一个binlog档案,也就是1G,按照这种增长速度,1天下来产生的binlog档案,就会占用大概144G左右的空间,磁盘空间可能很快就会被使用完。通过此配置,我们可以显示的控制binlog档案的数量,例如指定50,binlog档案最多只会占用50G左右的磁盘空间。
在更高版本的mysql中,支援按照秒级精度,来控制binlog档案的保留时间。下面我们将对binlog档案中的内容进行详细的讲解。
3.2 Binlog管理事件
所谓binlog管理事件,官方称之为binlog managent events,你可以认为是一些在任何模式下都有可能会出现的事件,不管你的配置binlog_format是Row、Statement还是Mixed。
以下通过"show binlog events"语法进行检视一个空的binlog档案,也就是只包含(部分)管理事件,没有其他资料更新操作对应的事件。如下:
在当前binlog v4版本中,每个binlog档案总是以Format Description Event作为开始,以Rotate Event结束作为结束。如果你使用的是很古老的Mysql版本中,开始事件也有可能是START EVENT V3,而结束事件是Stop Event。在开始和结束之间,穿插著其他各种事件。
在Event_Type列中,我们看到了三个事件型别:
Format_desc:也就是我们所说的Format Description Event,是binlog档案的第一个事件。在Info列,我们可以看到,其标明了Mysql Server的版本是5.7.10,Binlog版本是4。Previous_gtids:该事件完整名称为,PREVIOUS_GTIDS_LOG_EVENT。熟悉Mysql 基于GTID复制的同学应该知道,这是表示之前的binlog档案中,已经执行过的GTID。需要我们开启GTID选项,这个事件才会有值,在后文中,将会详细的进行介绍。Rotate:Rotate Event是每个binlog档案的结束事件。在Info列中,我们看到了其指定了下一个binlog档案的名称是mysql-bin.000004。关于"show binlog events"语法显示的每一列的作用说明如下:
Log_name:当前事件所在的binlog档名称Pos:当前事件的开始位置,每个事件都占用固定的字节大小,结束位置(End_log_position)减去Pos,就是这个事件占用的字节数。细心的读者可以看到了,第一个事件位置并不是从0开始,而是从4。Mysql通过档案中的前4个字节,来判断这是不是一个binlog档案。这种方式很常见,很多格式的档案,如pdf、doc、jpg等,都会通常前几个特定字元判断是否是合法档案。Event_type:表示事件的型别Server_id:表示产生这个事件的mysql server_id,通过设定my.cnf中的server-id选项进行配置。End_log_position:下一个事件的开始位置Info:当前事件的描述资讯3.3 Statement模式下的事件
mysql5.0及之前的版本只支援基于语句的复制,也称之为逻辑复制,也就是binary log档案中,直接记录的就是资料更新对应的sql。
假设有名为test库中有一张user表,如下:
现在,我们往user表中插入一条资料
insert into user(name) values("tianbowen");之后,可以使用"show binlog events" 语法检视binary log中的内容,如下:
红色框架中Event,是我们执行上面Insert语句产生的4个Event。下面进行详细的说明:
(划重点)首先,需要说明的是,每个事务都是以Query Event作为开始,其INFO列内容为"BEGIN",以Xid Event表示结束,其INFO列内容为COMMIT。即使对于单条更新SQL我们没有开启事务,Mysql也会预设的帮我们开启事务。因此在上面的红色框中,尽管我们只是执行了一个INSERT语句,没有开启事务,但是Mysql 预设帮我们开启了事务,所以第一个Event是Query Event,最后一个是Xid Event。
接着,是一个Intvar Event,因为我们的Insert语句插入的表中,主键是自增的(AUTO_INCREMENT)列,Mysql首先会自增一个值,这就是Intvar Event的作用,这里我们看到INFO列的值为INSERT_ID=1,也就是说,这次的自增主键id为1。需要注意的是,这个事件,只会在Statement模式下出现。
然后,还是一个Query Event,这里记录的就是我们插入的SQL。这也体现了Statement模式的作用,就是记录我们执行的SQL。
Statement模式下还有一些不常用的Event,如USER_VAR_EVENT,这是用于记录使用者设定的变数,仅仅在Statement模式起作用。如:
执行以下SQL:
set @name = \'tianshouzhi\';
insert into user(name) values(@name);这里,我们插入sql的时候,通过引用一个变数。此时检视binlog变化,这里为了易于观察,在执行show binlog events时,指定了binlog档案和from的位置,即只检视指定binlog档案中从指定位置开始的event。如下:
可以看到,依然符合我们所说的,对于这个插入语句,依然预设开启了事务。主键自曾值INSERT_ID=2。
当然,我们也看到了User var这个事件,其记录了我们的设定的变数值,只不过以16进位制显示。
3.4 Row模式下的事件
mysql5.1开始支援基于行的复制,这种方式记录的某条sql影响的所有行记录变更前和变更后的值。Row模式下主要有以下10个事件:
很直观的,我们看到了INSERT、DELETE、UPDATE操作都有3个版本(v0、v1、v2),v0和v1已经过时,我们只需要关注V2版本。
此外,还有一个TABLE_MAP_EVENT,这个event我们需要特别关注,可以理解其作用就是记录了INSERT、DELETE、UPDATE操作的表结构。
下面,我们通过案例演示,ROW模式是如何记录变更前后记录的值,而不是记录SQL。这里只演示UPDATE,INSERT和DELETE也是类似。
在前面的操作步骤中,我们已经插入了2条记录,如下:
现在需要从Statement模式切换到Row模式,重启Mysql之后,执行以下SQL更新这两条记录:
update user set name=\'wangxiaoxiao\';在binary log中,会把这2条记录变更前后的值都记录下来,以下是一个逻辑示意图:
该逻辑示意图显示了,在预设情况下,受到影响的记录行,每个字段变更前的和变更后的值,都会被记录下来,即使这个字段的值没有发生变化。
接着,我们还是通过"show binlog events"语法来验证:
首先我们可以看到的是,在Row模式下,单条SQL依然会预设开启事务,通过Query Event(值为BEGIN)开始,以Xid Event结束。
接着,我们看到了一个Table_map 事件,就是前面提到的TABLE_MAP_EVENT,在INFO列,我们可以看到其记录table_id为108,操作的是test库中user表。
最后,是一个Update_rows事件,然而其INFO,并没有像Statement模式那样,显示一条SQL,我们无法直接看到其变更前后的值是什么。
由于储存的都是二进位制内容,直接vim无法检视,我们需要借助另外一个工具mysqlbinlog来检视其内容。如下:
截图中显示了2个event,第一个红色框就是Table_map事件,第二个是Update_rows事件。
在第二个红色框架中,显示了两个Update sql,这是只是mysqlbinlog工具为了方便我们检视,反解成SQL而已。我们看到了WHERE以及SET子句中,并没有直接列出字段名,而是以@1、@2这样的表示字段位于数据库表中的顺序。事实上,这里显示的内容,WHERE部分就是每个字段修改前的值,而SET部分,则是每个字段修改后的值,也就是变更前后的值都会记录。
这里我们思考以下mysqlbinlog工具的工作原理,其可以将二进位制资料反解成SQL进行展示。那么,如果我们可以自己解析binlog,就可以做资料恢复,这并非是什么难事。例如使用者误删除的资料,执行的是DETELE语句,由于Row模式下会记录变更之前的字段的值,我们可以将其反解成一个INSERT语句,重新插入,从而实现资料恢复。
3.4.1 binlog_row_image引数
我们经常会看到一些Row模式和Statement模式的比较。ROW模式下,即使我们只更新了一条记录的其中某个字段,也会记录每个字段变更前后的值,binlog日志就会变大,带来磁盘IO上的开销,以及网络开销。
事实上,这个行为可以通过binlog_row_image控制其有3个值,预设为FULL:
FULL : 记录列的所有修改,即使字段没有发生变更也会记录。 MINIMAL :只记录修改的列。 NOBLOB :如果是text型别或clob字段,不记录这些日志。
我们可以将其修改为MINIMAL,则可以只记录修改的列的值。
3.4.2 binlog_rows_query_log_events引数
在Statement模式下,直接记录SQL比较直观,事实上,在Row模式下,也可以记录。mysql提供了一个binlog_rows_query_log_events引数,预设为值为FALSE,如果为true的情况下,会通过Rows Query Event来记录SQL。
可以在my.cnf中新增以下配置,来开启row模式下的原始sql记录(需要重启):
binlog-rows-query-log_events=1之后,再插入资料资料时
insert into user(name) values("maoxinyi");在binlog档案中,我们将看到Rows Query Event
3.5 GTID相关事件
从MySQL 5.6开始支援GTID复制。要开启GTID,修改my.cnf档案,新增以下配置
gtid-mode=onenforce-gtid-consistency=true在这种情况下,每当我们执行一个事务之前,都会记录一个GTID Event
insert into user("name") values("zhuyihan");此时binlog内容如下:
而当我们切换到下一个binlog档案时,会记录之前的已经执行过的GTID。这里我们通过执行以下sql手工切换到一个新的binlog档案。
mysql> flush logs;Query OK, 0 rows affected (0.00 sec)之后在新的binlog档案中,我们看到之前执行过的GTID在下一个档案中出现了。
本文不是专门讲解GTID的文章,感兴趣的读者,可以自行检视相关资料。
4 总结
本文对mysql binlog的应用场景进行了深入的讲解,并介绍了mysql中大部分binlog event的作用。如果读者想更加深入的去学习,例如如何模拟mysql的slave去解析binlog,可以参考一些开源的实现,不过这些生产级别的元件,因此通常程式码比较复杂。笔者自己也造过类似的轮子,仅仅模拟slave去拉取mysql的binlog,并对事件进行解析,对于理解binlog解析的核心原理应该有一些帮助。
https://mp.weixin.qq.com/s/-CXTVPkUdMkT-6PB3lHLRw





























