在很多场景下,MySQL 的高可用都是借助主从复制实现的,而 MySQL 复制不断的演进,也使得她越来越受欢迎。这一节内容就来聊聊 MySQL 复制的演进。

1 三种日志格式对复制的影响

1.1 开始支持复制

MySQL 从 3.23 版本开始支持复制,但是在 5.1.5 之前只支持 statement 格式的复制,尽管这种模式下,binlog 日志量相对比较少,但是涉及到跨库更新、或者使用结果不确定的函数时,比如 UUID(),容易出现主从数据不一致的情况。

 

1.2 开始支持 Row 格式的复制

从 MySQL 5.1.5 开始,新增了 Row 格式,日志中会记录每一行数据被修改的形式,因此 Row 格式下的复制,主从之间的数据一致性保障得到了大幅度提升,但是缺点是 binlog 日志量相对 statement 较多。

 

1.3 新增 mixed 格式

从 MySQL 5.1.8 开始新增 mixed 格式,当可能造成主从数据不一致的 SQL 时,binlog 使用 row 格式记录,否则使用 statement 格式记录。显然这种格式下,日志量是介于 statement 和 row 格式之间的。

 

2 半同步复制

2.1 异步复制

传统的 MySQL 复制为异步复制,其原理如下:

  • 在主库开启 binlog 的情况下;

  • 如果主库有变更操作,会记录到 binlog 中;

  • 主库通过 IO 线程把 binlog 里面的内容传给从库的中继日志(relay log)中;

  • 主库给客户端返回 commit 成功(这里不管从库是否已经收到了事务的 binlog);

  • 从库的 SQL 线程负责读取它的 relay log 里的信息并应用到从库数据库中。

其过程如下图:

复制的演进历程_客户端

在异步复制下,假如配置了自动切换的前提下,主库突然宕机,然后从提升为主时,原来主库上可能有一部分已经完成提交的数据还没来得及发送到从库,就可能产生数据丢失。为了解决这个问题,在 MySQL 5.5 版本中引入了半同步复制。下面来看下半同步复制的原理。

 

2.2 半同步复制

半同步复制原理如下:

  • 在主库开启 binlog 的情况下;

  • 如果主库有增删改的语句,会记录到 binlog 中;

  • 主库通过 IO 线程把 binlog 里面的内容传给从库的中继日志(relay log)中;

  • 从库收到 binlog 后,发送给主库一个 ACK,表示收到了;

  • 主库收到这个 ACK 以后,才能给客户端返回 commit 成功;

  • 从库的 SQL 线程负责读取它的 relay log 里的信息并应用到从库数据库中。

其过程如下图:

复制的演进历程_同步复制_02

跟传统异步复制相比,半同步复制保证了所有客户端发送过确认提交的事务,从库都已经收到这个日志了。

但是这种模式下,实际上主库已经将该事务 commit 到事务引擎层,只是在等待返回而已,而此时其他 session 已经可以看到数据发生了变化,如果此时主库宕机,有可能从库还没写 Relay log,就会发生其他 session 切换前后查询的数据不一致的情况。

因此,从 MySQL 5.7 开始,引入了增强半同步复制(无损复制)。

 

2.3 增强半同步复制

增强半同步复制(也叫无损复制)是在半同步复制基础上做了微调,即主库写数据到 binlog 后,就开始等待从库的应答 ACK,直到至少一个从库写入 Relay log 并进行数据落盘后,才返回给主库消息,通知主库可以执行 commit 操作了,然后主库开始提交到事务引擎层。

但是,增强半同步复制其实也是存在问题的,假设有一个事务写 binlog 后 crash 了,事务还没有发送给从库,这时从库提升为主库,对客户端来说,数据没问题的,因为主库还没给客户端返回 commit;但是当老的主库恢复后,由于这个事务的 binlog 已经写入磁盘了,因此没办法回滚,在 crash recover 的机制下,会把这些事务重新提交,这就导致老的主库比新的主库多事务的情况。

因此又出现了一种复制形式--组复制。

 

3 组复制

MySQL 5.7 推出了组复制(MySQL Group Replication,简称:MGR),是基于内置的主从复制的架构实现的,主要在事务提交的过程中,嵌入单独的 binlog 封装逻辑,并通过专门的复制通道进行数据传输,

Group Replication 复制插件使用 Paxos 协议的原子广播特性,来保证在集群内的大多数节点都能接收到数据包,当数据节点接收到 write set 之后,每个节点上按照相同的规则对事务进行排序,并进行冲突检测。

对于 master,当冲突检测通过之后,数据变更写入自身的 binlog 中,然后进行存储引擎层的提交(如果发现事务冲突,则进行事务回滚);

对于 slave,当冲突检测通过之后,就把主库发来的 binlog 写入自身的 relay log 中,然后 sql 线程读取 relay log 进行重放,并把重放的 binlog 日志写入自身的 binlog 中,然后存储引擎内部进行提交(如果发现事务冲突,则丢弃主库发送过来的 binlog 日志) 。

复制的演进历程_同步复制_03

那么组复制如何处理故障转移的呢?

假设 3 个节点的集群,当写节点 crash 之后,集群内部重新选举一个节点作为新的写节点,整个写请求故障转移过程都是 Group Replication 内部自动完成(当然,写请求具体发送到哪个节点,还是需要更新 DNS 解析或者使用其它插件),解决了某个节点挂了如何踢掉或者恢复后如何加入集群(异步复制和半同步复制都需要人为干预或者依赖其他插件)。 

 

4 并行复制

4.1 MySQL 5.6 的并行复制

在传统的复制模式下,我们也许经常会遇到主从延迟的场景。这是因为在 MySQL 5.6 之前,MySQL 只支持单线程复制。

从 MySQL 5.6 版本开始,支持并行复制策略,但是只支持库级别的。如果表都集中在一个 DB 里,或者热点表集中在一个库中,那就没有什么效果了。

 

4.2 MySQL 5.7 的并行复制

由参数:slave-parallel-type 控制并行复制策略。

配置为 DATABASE,表示使用 MySQL 5.6 版本的按库并行策略;

配置为 LOGICAL_CLOCK,同时处于 prepare 状态的事务,在备库执行时是可以并行的;处于 prepare 状态的事务,与处于 commit 状态的事务之间,在备库执行时也是可以并行的。

 

4.3 MySQL 5.7.22 的并行复制

MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制。

相应地,新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略。这个参数的可选值有以下三种。

  • COMMIT_ORDER,表示的就是前面介绍的,根据同时进入 prepare 和 commit 来判断是否可以并行的策略。

  • WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset。如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行。

  • WRITESET_SESSION,是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。

 

5 基于 GTID 的复制

通过 sql_slave_skip_counter 跳过事务和通过 slave_skip_errors 忽略错误的方法,虽然都最终可以建立从库 B 和新主库 A’的主备关系,但这两种操作都很复杂,而且容易出错。MySQL 5.6 版本引入了 GTID,解决了这个问题。并且 GTID 复制模式,让我们配置主从更加简单。基于 GTID 的复制配置及维护后续再细讲。