(二十四)全解MySQL之主从篇:死磕主从复制中数据同步原理与优化

引言

兜兜转转,经过《全解MySQL专栏》前面二十多篇的内容讲解后,基本对MySQL单机模式下的各方面进阶知识做了详细阐述,同时在前面的《分库分表概念篇》、《分库分表隐患篇》两章中也首次提到了数据库的一些高可用方案,但前两章大多属于方法论,并未涵盖真正的实操过程。接下来的内容,会以目前这章作为分割点,开启MySQL高可用方案的落地实践分享的新章程!

一、主从复制架构概述

不论任何技术栈,但凡一提高可用、高性能、高稳定这些词汇,必然会牵扯到集群、主从架构的概念,如MQ、Redis、ES、MongoDB、Zookeeper.....任何技术栈中都会有,而MySQL中同样不例外,官方也提供了主从架构的支持,通过调整多个MySQL节点的配置信息,即可将一个节点的数据同步给另一个或多个节点,但这种方式同步的是所有数据!

有人也许会疑惑:为什么要同步所有数据呀?难道不能对数据作分片,不同的节点同步不同的数据嘛?这实际上属于历史遗留性的问题,在早些年由于分布式技术还未完善,所以大多技术栈在设计高可用方案时,基本上都选用主从架构,也就是各节点之间都同步所有数据,从而对外提供高可用的保障。

主从架构中必须有一个主节点,以及一个或多个从节点,所有的数据都会先写入到主,接着其他从节点会复制主节点上的增量数据,从而保证数据的最终一致性,使用主从复制方案,可以进一步提升数据库的可用性和性能:

①在主节点宕机或故障的情况下,从节点能自动切换成主节点的身份,从而继续对外提供服务。

②提供数据备份的功能,当主节点的数据发生损坏时,从节点中依旧保存着完整数据。

③可以基于主从实现读写分离,主节点负责处理写请求,从节点处理读请求,进一步提升性能。

但无论任何技术栈的主从架构,都会存在致命硬伤,同时也会存在些许问题需要解决:

①硬伤:木桶效应,一个主从集群中所有节点的容量,受限于存储容量最低的哪台服务器。

②数据一致性问题:由于同步复制数据的过程是基于网络传输完成的,所以存储延迟性。

③脑裂问题:从节点会通过心跳机制,发送网络包来判断主机是否存活,网络故障情况下会产生多主。

上述提到的三个问题中,第一个问题只能靠加大服务器的硬件配置解决,第二个问题相对来说已经有了很好的解决方案(后续讲解),第三个问题则是部署方式决定的,如果将所有节点都部署在同一网段,基本上不会出现集群脑裂问题。

上面简单了解了主从复制架构的一些基本概念后,接着来聊一聊MySQL中的主从复制。

二、MySQL中的主从复制技术

MySQL本身提供了主从复制的技术支持,所以无需通过第三方技术来协助实现,还记得在《MySQL日志篇》中聊到的Bin-log二进制/变更日志嘛?MySQL数据复制的过程就是基于该日志完成的,但MySQL复制数据的过程并非同步,而是异步的方式,啥是同步、异步方式呢?如下:

同步复制和异步复制

同步复制和异步复制

上图中,上面是同步复制的过程,也就是当主节点接收到一个客户端的写请求后,先会往自身写入数据,接着会去要求其他从节点也写入数据,当从节点的数据写入完成后,最终才会向客户端返回写入成功。

而异步复制的过程恰恰相反,当主节点接收到一个客户端的写请求后,先会往自身写入数据,自身数据写入成功后,就会立马向客户端返回写入成功的信息,对于从节点的数据,会在其他的时间内再异步复制过去。

对比同步复制和异步复制的过程,显然同步复制的过程极其影响效率,因为需要等到所有从节点写完之后才返回数据。而异步复制的过程则快很多了,基本和MySQL单机版的写入效率差距不大,只要主节点自身写入成功,则立马返回写入成功,性能自然会快很多,而MySQL主从的数据复制,就是基于这种异步方案实现。

有人也许会好奇:同步复制方案效率那么低,一般没有谁用吧?实则不然,比如大名鼎鼎的Zookeeper中,就采用的是同步复制方案,这是为什么呢?因为同步复制方案能够确保数据100%不丢失,和数据的强一致性。如果采用异步复制方案,那么当一个数据写入后,主节点立马宕机或故障了,从机顶替上线,这时的从机是不一定有最新数据的,而同步方案则不需要担心这个问题(Zookeeper采用的是半同步模式,后续再展开细聊~)。

2.1、MySQL数据同步的原理

前面搞清楚了MySQL同步数据的方式,接着聊一聊它的同步原理,前面简单提到过一句:MySQL是基于它自身的Bin-log日志来完成数据的异步复制,因为Bin-log日志中会记录所有对数据库产生变更的语句,包括DML数据变更和DDL结构变更语句,数据的同步过程如下:

主从复制原理

主从复制原理

上述即是主从同步数据的原理图,但在讲解之前先来了解一下两种数据同步的方式:

主节点推送:当主节点出现数据变更时,主动向自身注册的所有从节点推送新数据写入。

从节点拉取:从节点定期去询问一次主节点是否有数据更新,有则拉取新数据写入。

那MySQL究竟采用的是什么方式呢?其实是「从拉」的方案,但对其稍微做了一些优化,传统的「从拉」方案是需要从节点一直与主节点保持长连接,从节点定时或持续性的对主节点做轮询,查看主机的数据是否发生了变更,而MySQL的数据同步原理如下:

①客户端将写入数据的需求交给主节点,主节点先向自身写入数据。

②数据写入完成后,紧接着会再去记录一份Bin-log二进制日志。

③配置主从架构后,主节点上会创建一条专门监听Bin-log日志的log dump线程。

④当log dump线程监听到日志发生变更时,会通知从节点来拉取数据。

⑤从节点会有专门的I/O线程用于等待主节点的通知,当收到通知时会去请求一定范围的数据。

⑥当从节点在主节点上请求到一定数据后,接着会将得到的数据写入到relay-log中继日志。

⑦从节点上也会有专门负责监听relay-log变更的SQL线程,当日志出现变更时会开始工作。

⑧中继日志出现变更后,接着会从中读取日志记录,然后解析日志并将数据写入到自身磁盘中。

阅读上述流程后,相信大家能感受出MySQL主从复制时做的优化,在主从数据同步的过程中,从节点并不会无限制的询问主机,这样实在太影响效率了,在MySQL中引入了惰性的思想,只有当主节点真正出现数据变更时,才会通知从节点拉取数据!

2.2、从节点拉取的数据到底是什么格式?

前面从整体角度出发,简单讲述了主从同步数据的过程,但从节点复制的数据到底是什么格式的呢?这里要根据主节点的Bin-log日志格式来决定,在《日志篇》中详细聊过这个日志,它会有三种格式,如下:

Statment:记录每一条会对数据库产生变更操作的SQL语句(默认格式)。

Row:记录具体出现变更的数据(也会包含数据所在的分区以及所位于的数据页)。

Mixed:Statment、Row的结合版,可复制的记录SQL语句,不可复制的记录具体数据。

一般在搭建主从架构时,最好将Bin-log日志调整为Mixed格式,因为这种方式绝对不会出现数据不一致性,毕竟默认的Statment格式会导致主从节点间的数据出现不一致,例如:

insert into `zz_users` values(11,"棕熊","男","3333",now());

当主节点插入数据时,使用了sysdate()、now()....这类函数时,主节点会取自身的系统时间插入数据,而当从节点拉取SQL执行时,则会获取从节点的系统时间插入数据,因为主/从节点执行SQL语句绝对不可能发生在同一时间,因此就会导致主/从节点中,同一条数据的时间不一致。

而将Bin-log日志调整为Mixed格式后,就不会再出现这样的问题,因为对于这种不可复制的记录,会直接选择记录具体变更过的数据到日志中,当从节点读取数据写入时,则可以直接将数据放到磁盘对应的位置中即可。

那为什么不选择Row格式来作为数据同步时的格式呢?这种方式同步数据不需要经过SQL解析过程,只需直接将数据放到磁盘的具体位置即可,但这种方式会让主节点的I/O负载直线拉高,在传输时也会大量占用网络带宽,因此一般都不会选择Row作为同步复制时的格式。

2.3、基于主从模型的不同架构

前面将最基本的主从复制原理和细节讲清楚了,虽然MySQL官方只提供了最基本的主从复制技术的支持,但这并不妨碍咱们将其玩出花来,目前业内可以基于主从机制实现:一主一从/多从、双主/多主、多主一从、级联复制四种架构,下面详细聊一聊每种架构。

2.3.1、一主一从/多从架构

一主一从或一主多从,这是传统的主从复制模型,也就是多个主从节点组成的集群中,只有一个主节点,剩余的所有节点都为其附属关系,大致如下:

主从读写分离_v2

主从读写分离_v2

这种架构中,从节点的所有数据都源自于主节点,如上图所示,为了充分的利用好这种架构,一般都会基于它实现读写分离,也就是将客户端的写请求发给主节点处理,将客户端的读请求发给从节点处理。这种模式下,相较于单机节点而言,能够在性能上进一步提升,因为读写请求都被分发到了不同的节点处理,所以吞吐量至少会提升50%以上。

2.3.2、双主/多主架构

上述的一主一从/多从架构,更加适用于一些读大于写的场景,因为这类项目中,读请求的数量远超出写请求,因此将读操作分发到从库上,能够大大降低主库的访问压力,但如若你的项目中读写请求的比例对半开,同时整体的并发量也不算低,至少超出了单库的承载阈值,这时就可以选用双主/多主架构,如下:

双主双写架构_v2

双主双写架构_v2

上图是一个典型的双主架构,这里有两个MySQL节点,它们都是主库,也都属于对方的从库,也就是两者之间会相互同步数据,这时为了防止主键出现冲突,一般都会通过设置数据库自增步长的方式来防重,通常会将两个节点的自增步长设为2,然后为两个节点分配自增初始值1、2,最终会出现如下效果:

DB1:1、3、5、7、9、11、13、15、17.......

DB2:2、4、6、8、10、12、14、16、18......

在两个库上插入数据时,各自会以上述序列进行主键的递增,接着两个节点会各自同步对方的数据,这也就意味着双方都具备完整的数据,因此无论是:简单读、简单写、批量写、多表查......等任何类型的操作,都可以随便分发到任意节点上处理。

2.3.3、多主一从架构

除开上述两种主从架构的基本玩法外,面对于不同场景时也会有一些新奇玩法,比如面对于一些写大于读的场景,不管是一主多从、还是多主的方式,似乎都无法很好的解决这个问题,因此要处理这类问题时,可以选用多主一从架构,啥意思呢?如下图:

多主一从架构

多主一从架构

因为这种架构要解决写大于读造成的性能瓶颈,所以这里会采用多主来承载客户端的写请求,同时为了做好主键防重,也需要设置数据库自增步长和初始值,这里有三个主节点,因此自增步长为三,初始值分别为1、2、3,最终三个主节点中的数据如下:

DB1:1、4、7、10、13、16.....

DB2:2、5、8、11、14、17.....

DB3:3、6、9、12、15、18.....

而这三个主节点都会具备一个相同的从节点,也就是只有一个从库,它会负责同步所有主库的数据,这也就意味着从库中会具备三个主库的完整数据,这样做有什么好处呢?通过多个主节点解决了写请求导致压力过大的问题,同时从库中还有完整数据,因此也不会由于拆分了主库而影响读操作。

2.3.4、级联复制架构

级联复制架构是一种特殊的主从结构,之前聊到的几种主从结构都只有两层,但级联复制架构中会有三层,关系如下:

级联复制架构

级联复制架构

也就是在级联复制架构中,存在两层从库,这实际上属于一主多从架构的升级版,毕竟如果一个主节点存在多个从节点时,多个从节点都会同时去主节点拉取新数据,如果数据量较大,就会导致主节点的I/O负载过高,因此这种级联复制架构要解决的问题,也就是多个从库会对主库造成太大压力的问题。

这个过程中,第一层从库只有一个节点,它会负责从主库上拉取最新的数据,接着第二层的多个从库会从上一层的从库中拉取数据,这样能够这在从库较多的情况下,尽量降低数据同步对主节点的性能影响。

2.3.5、主从架构小结

前面聊完了四种主从架构的模式,下面做个简单的小总结:

一主多从架构:适用于读大于写的场景,采用多个从库来分担数据库系统的读压力。

多主架构:适用于读写参半的场景,采用多个主库来承载数据库系统整体的读写压力。

多主一从架构:适用于写大于读的场景,采用多个主库分担写压力,单个从库承载读压力。

级联复制架构:适用于读大于写的场景,采用单个从节点来分担从库对主库造成的I/O压力。

三、主从复制数据的方式

在第二阶段时,曾简单的聊过两种主从复制数据的方式,即同步与异步方式,但MySQL中一共支持四种数据同步方式,即:异步复制、同步复制、半同步复制、增强式半同步复制/无损复制,其中对于同步、异步复制已经在前面聊了,接着再聊一聊后面两者。

3.1、半同步式复制

虽然同步复制的模式能够确保数据的强一致性,但由于其太影响写入效率,因此很少有主从机制会采用这种模式同步数据,包括MySQL默认也是采用异步复制方案,不过异步模式虽然性能不错,但特殊情况下会导致数据出现丢失,因此也会存在风险,这时半同步模式就出现了!

半同步模式:当客户端的数据到来时,会先写入到主节点中,接着主节点会向所有从节点发送一个写入数据的请求,但半同步模式中无需等待所有从节点全部写入完成后再返回,而是只要有一个从节点写入成功并返回了ACK,则会直接向客户端返回写入成功,这样既能够保证性能,又能够确保数据不丢失。

同时为了避免网络延迟造成主库长时间收不到从库的ACK,因此在配置半同步式复制时,会有一个rpl_semi_sync_master_timeout参数来控制超时时间,其默认值是10000ms/10s,如若主库在10s内依旧未收到从库的ACK,则会将复制模式切换成异步模式,切成异步模式后,会在后续网络正常后再次切回半同步模式。

但实际上这种只要求一个节点返回ACK的半同步模式,依旧存在数据丢失风险,所以Zookeeper中,是要求收到一半从节点以上的ACK时,也就是一半从节点以上写入数据成功后,才会向客户端返回写入成功,至于为什么是一半写入成功后就返回,具体原因跟其内部的ZAB一致性协议有关,这里就不展开叙述了。

3.2、增强式半同步复制/无损复制

增强式半同步复制也被称为无损复制,这是MySQL5.7版本中引入的一种新技术,在MySQL5.7版本中就不存在普通的半同步模式了,当将复制模式配置成半同步时,默认就会选用无损复制模式,和之前传统的半同步复制区别在于:从after-commit变成了after-sync,啥意思呢?

after-commit:主库在未收到从库的ACK之前,虽然不会给客户端返回写入成功,但本质上在MySQL中会提交事务,也就是主库中的其他事务是可以看见对应数据的,当此时出现宕机时,就会导致旧主上能查询出的数据,在新主(原本的从库)上无法查询出来了。
after-sync:当主库未收到从库的ACK之前,也不会在主库上提交事务,也就是保证了主从节点的数据强一致性,解决了after-commit中存在的问题。

其实简单来说,无损复制中等待ACK的动作会放到事务提交前进行,而传统半同步复制中,等待ACK的动作会放到事务提交后进行。两者之间的区别就在于:对主从节点的数据严格性不同,一般情况下,无损复制会比传统半同步复制开销更大一些,因为事务迟迟不提交,会导致对应的锁资源不会主动释放,其他需要获取对应锁资源的事务只能阻塞等待,这会造成主库的整体性能出现一定影响。

上面聊完了主从复制的四种数据同步方式,接着来聊一聊MySQL5.6中,两种新的复制特性,即从库数据的延迟复制,和从库数据的并行复制,展开说说吧~

3.3、延迟复制

延迟复制通常用于一些特殊场景,它可以支持从库数据的延迟同步,也就是当从库上的I/O线程,将主库的Bin-log日志请求回来后,从节点的SQL线程并不会立刻解析日志执行,而是等待一段时间后再解析日志执行,这个等待的时间可以由开发者来配置,一般建议设为3~6小时之间。

那延迟复制的好处在于什么呢?可以防止误删操作,如若在主库上不小心误删了大量数据、表、库或其他数据库对象,因为从库并不是立即执行同步过去的记录,因此可以及时通过从节点上的数据回滚数据。除此之外,也能对一些线上Bug进行实时观测,比如一个无法复现的故障问题发生时,如果发现时还在配置的延迟复制时间内,则可以去到从库上观察。

但开启延迟复制,基本上就无法再对主从架构做读写分离,毕竟很少有业务能够接受3~6小时的数据延迟,因此延迟复制的技术,通常都仅适用于一些作为备库的节点使用。

3.4、并行复制

并行复制是在MySQL5.6加入的新技术,但实际上5.6版本中的支持并不完善,直到5.7版本中才真正实现了并行复制技术,但想要说清楚并行复制,则得先弄懂组复制的概念,想要弄明白组复制,在此之前还得理解GTID复制的概念,所以并行复制技术会比前面几个概念难啃一些。

3.4.1、GTID复制

GTID复制依旧是5.6版本中的新功能,在传统的主从架构中,当需要发生主从切换时,需要开发/运维人员手动找到Bin-log的POS同步点,然后执行change master to [new-master-pos]命令,将其他从节点指向新主库,但每个从节点可能同步数据的进度都不一致,因此每个从节点都需要去找到它上次的POS点,然后指向新主库,这个工作是不是听起来就比较繁杂?答案是Yes,不过到了MySQL5.6版本后,开启了GTID复制后,则无需手动寻找POS点!

GTID(Global Transaction ID)也就是全局事务标识符的意思,它由节点UUID+事务ID两部分组成,MySQL在第一次启动时都会利用UUID随机生成一个server_id,还记得在之前的《MVCC机制》中聊过的事务ID嘛?MySQL会对每一个写事务都分配一个顺序递增的值作为事务ID,而GTID则是由这两玩意儿组成的,格式为server_uuid:trx_id。

当主库的事务有了这个全局事务标识后,再发生主从切换时就无需手动寻点了,仅需要执行change master to master_auto_position = 1这条命令即可,它会自动去新主库上寻找数据的同步点,也就是MySQL自身就具备断点复制的功能。

为什么需要用GTID代替POS同步点呢?

因为同一个主从集群中,所有节点加入集群的时间可能会不同,比如下面的这个主从集群:

主从集群

主从集群

假设这个集群中每个节点加入的时间都不一致,如:

master:日志文件中的同步点POS=1200。

slave1:日志文件中的同步点POS=1100。

slave2:日志文件中的同步点POS=800。

slave3:日志文件中的同步点POS=200。

此时假设master节点宕机或故障了,slave1成为了新主,那么slave2、slave3也应该成为新主slave1的从节点,但此刻问题就来了:原本slave2、slave3的POS同步点是基于master中的日志而言的,但现在主节点变成了slave1,这时slave2、slave3如何去寻找自己在slave1中的POS点呢?显然MySQL无法自己完成该工作,因此需要人工指定同步点才行。

而GTID出现的原因,就是为了解决上述这个问题,但具体怎么解决的呢?接下来重点聊一聊这个。

GTID的工作过程

master在更新数据时,会为每一个写事务分配一个全局的GTID,并记录到Bin-log中。

slave节点的I/O线程拉取数据时,会将读到的记录写到relay-log中,并设置gtid_next值。

slave节点的SQL线程执行前,会读取gtid_next值得知接下来该解析哪条日志并执行。

slave节点的SQL线程在执行时,会先比对自身的Bin-log日志中是否有对应的GTID:

有:意味着该GTID对应的事务已经执行过了,slave会自动忽略掉这条记录。

没有:SQL解析该GTID对应的relay-log记录并执行,再将GTID记录到Bin-log。

GTID自动寻找同步点的原理

前面得知了GTID的大致工作过程,开启GTID后,从库会基于它来复制主库的数据,此时发生了主从切换,假设这时主从集群中有多个从节点,MySQL首先会选择距离master的GTID最近的从节点作为新主,然后将其他从节点转变为新主的从库,其他从库会根据自身gtid_next值,去新主的日志文件中做对比,然后找到各自的同步点,继续从新主中复制数据。

因为MySQL挑选的是和旧主GTID值,最接近的从节点作为新主,也就意味着作为新主的从节点,绝对会比其他从节点的数据要完善,因此新主中的GTID值也是最大的!同时,每个从节点中都存在一个gtid_next值,记录着自身下一次要同步数据的GTID值,此时剩下的从节点就可以根据该值,直接去新主中寻找到自己的同步点位置,从而避免了之前那种手动介入的尴尬场景出现。

不过由于GTID复制是基于事务来实现的,这也就代表不支持事务的存储引擎无法使用这种机制,在之前的章节中也聊过,MySQL众多存储引擎中,基本上只有InnoDB支持事务,所以GTID机制基本上只对InnoDB引擎生效。

3.4.2、组复制

前面阐述的GTID复制则是组复制的实现基础,而组复制则是并行复制的基础,那么什么叫做组复制呢?组复制是指将一组并行执行的事务,全部放入到一个GTID中记录,后续从节点同步数据时,会一次性读取这一组事务解析并执行,与传统的GTID区别如下:

传统的GTID值由节点ID+事务ID组成:12EEA4RD6-45AC-667B-33DD-CCC55EF718D:88。

组复制的GTID通过逗号分隔:12EEA4RD6-45AC-667B-33DD-CCC55EF718D:89, 12EEA4RD6-45AC-667B-33DD-CCC55EF718D:89-94, ......。

MySQL如何实现事务分组的呢

看过MySQL源码的小伙伴应该清楚,MySQL提交事务时内部会调用ordered_commit函数来处理相关工作,其函数执行的逻辑流程图如下:

事务组提交原理

事务组提交原理

当一个事务提交时都会调用ordered_commit函数,首先会将事务加入等待事务组,接着会经过三个核心步骤:FLUSH、SYNC、COMMIT,对应的也会有三个队列,它们三者的工作原理都大致相同:

①如果某个事务进入FLUSH队列时,该队列还是空的,则这个事务会担任“队长”的角色。

②当后续其他事务进入队列时,发现队列不为空,则会将提交工作委托给队长来完成。

③如上图中的「事务1」则是队长,后续的都是队员,但队长不会无限制等待队员到来:

从队长加入的时间点开始,当超出binlog_group_commit_sync_delay规定的时间后,就会进行一次组提交。

同一时刻只允许一组事务做这些工作,也就是当有另外一组事务提交时,需要等待上一组事务提交完成。

在做组提交工作时,会将当前事务组的内容记录到Bin-log日志中,同时会将这组事务记录成一个GTID,不同事务之间通过,逗号分隔(实际过程更为复杂,这里只做简单讲解)。

经过上述步骤后,就实现了按事务组去生成GTID值,这也是后续并行复制的基础,接着往下看。

3.4.3、并行复制

在MySQL5.6之前的版本中,从库同步数据时,所有数据同步工作都是基于单线程完成的,也就是不管主库上的数据是不是多线程并发写入的,从库上只会有一条SQL线程来执行解析执行工作。

到了MySQL5.6之后,引入了并行复制的思想,但5.6中的并行复制极其鸡肋,基本无人问津,因为是基于库级别的并行复制,也就是一个从节点对应多个主节点时,有几个主节点就开几条SQL线程去解析并写入数据,即多主一从架构中才会用到。

因为官方最初在实现并行复制时,一直纠结锁冲突的问题,所以为了防止并行执行时出现数据冲突,就造出了上面那种库级别的并行复制,为啥要纠结锁/数据冲突呢?比如从库I/O线程在主库中请求了100条记录回去,从库中开100条SQL线程解析并执行这些记录,如果其中有两条记录操作的是同一条数据,就会出现锁冲突问题。

但上述原因不是最主要的,最主要的是并发执行的顺序问题,如果主库上对于一条数据是先改后删,从库在并发执行时,因为多线程执行的无序性,把执行顺序改为了先删后改,这显然就会导致数据冲突,因此变更操作很难实现并行复制。

到了MySQL5.7中,才基于组复制技术实现了真正意义上的并行复制,因为能够在同一时间内提交的事务,绝对是不存在锁冲突的,所以可以开启多条线程同时执行一个组中不同的事务,但这个思想是从MariaDB中照抄过来的~

一句话来总结就是:主库上是咋样并发写入数据的,从库也会开启对应的线程数去并发写入。

在5.7中官方为这种机制命名为enhanced multi-threaded slave,简称MTS机制,同时为了兼容5.6版本中的并行复制,又多加入了一个slave-parallel-type参数:

DATABASE:默认的并行复制模式,表示基于库级别的来完成并行复制。

LOGICAL_CLOCK:表示基于组提交的方式来完成并行复制。

但当你想要使用这种并行复制的技术,必须要将版本升级到MySQL 5.7.19才行,因为在此之前的版本中,MTS技术依旧存在不小瑕疵。

并行复制出现的意义是什么?

对于这个问题相信大家都能直接想出来答案,能够在很大程度上提升从库复制数据的速度,也就是能够让从库的数据实时性提升,尤其是无损复制模式中,主节点需要等待从节点的ACK才会真正提交事务,从库使用并行复制后,能够在一定程度上解决从库的复制延迟问题。

不过虽然5.7中的并行复制,在一定程度上解决了原有的从库延迟问题,但如果一个新的从节点加入集群时,因为要从头开始同步数据,这种并行复制的模式依旧存在效率问题,而到了MySQL8.0中,对于并行复制技术提出了真正的解决之道,也就是基于writeset的MTS技术。

啥叫基于writeset的MTS技术呢?即多个事务之间,只要变更的数据记录没有重叠,也就是操作的数据没有冲突,无需在一个事务组内,也可以支持并发执行,这也是MySQL-MTS技术的最终的完美形态。

四、主从数据一致性的解决方案

到这里为止,对于主从复制架构中绝大多数知识点和细节技术,都已经做了全面概述,最后再来重点叙说一个点,则是主从节点之间的数据一致性问题。通常情况下,对MySQL搭建了主从集群,往往从节点不会只充当备库的角色,为了充分利用已有的资源,都会在主从热备的基础上,采用读写分离方案,将写请求分发给主库、读请求分发给从库处理。

这样的确能够充分发挥从库所在机器的性能,但随之而来的还有一些问题,由于从库需要处理读取数据的请求,但主从节点之间的数据绝对会有一定延时,那对于一些关键性的数据,在主库上修改之后,再去从库上读取,这时就很有可能读取到的是旧数据,即修改前的原数据。

但这种情况对于用户而言显然是无法接受的,比如一个用户将个人信息修改后,然后再次查看时,发现个人信息依旧是修改之前的原数据,这时用户就有可能再次修改,经过反反复复多次修改后,用户发现依旧未生效,大多数情况下都会心里骂一句:“去你*的,乐色系统”!

想要解决上述这种读写分离导致的数据不一致性,主要有四种解决方案:

业务逻辑做改变、复制方式做更改、数据库架构做调整、引入第三方中间件。

4.1、改变业务逻辑

改变业务逻辑的潜在含义是:对业务做一定更改,比如当用户立即修改数据后,因为在从库读不到数据,所以先显示一个审核状态,这样能够给出用户的反馈,从而避免用户再次重复操作,如:

上述是掘金的个人资料,当用户手动更改后,会先显示一个「审核中」的状态,这种方式属于和数据不一致妥协的方案,接受一定的数据延迟,不过这种方式并不适用于一些对数据实时性要求较高的场景。

4.2、更改复制方式

在之前曾聊过MySQL四种数据同步复制的方式,即全同步、异步、半同步与无损复制,MySQL默认会是异步复制模式,即主节点写入数据后会立即返回成功的状态给客户端,这样能够确保性能达到最佳,但如果对实时性要求较高,可以将其改为全同步或半同步模式。

改成全同步的模式能够确保数据100%不会存在延迟,但性能会因此严重下降,因此半同步是个不错的选择,但如果数据库仅仅是一主一从的架构,那么半同步模式和全同步模式没有任何区别,因为半同步是至少要求一个从库返回ACK,才向客户端返回写入成功,而一主一从架构中只有一个从节点~

不过就算是半同步(无损)复制模式,对比异步复制而言,依旧会导致性能下降较为严重。

4.3、调整数据库架构

如果无法接受主从架构带来的短期数据不一致,那可以升级服务器硬件,并将架构恢复成单库架构,所有读写操作都走单库完成,这就自然不会出现数据不一致问题。但如果升级硬件配置后,无法承载客户端的访问压力时,可将整体架构升级到分库分表架构,制定好合适的分片策略和路由键,每次读写数据都根据业务不同,操作不同的库。

但如果项目的业务规模目前或未来两三年内达不到分库分表的规模,这会导致性能过剩的问题出现,也就是浪费机器性能(可以通过容器化技术解决,不过分库分表会导致更多的问题产生,关于这点在《分库分表后遗症》中详细聊过)。

4.4、引入第三方中间件

前面聊到的三种方案,多多少少都存在一些局限性,因为要么接受数据不一致、要么损失性能、要么使用更高规模的架构处理,但这些方案似乎都存在令人不能接受的后患,那有没有一种万全之策来解决这个问题呢?答案是有的,就是引入Canal中间件来监控主节点的Bin-log日志。

在之前讲主从同步数据原理时,曾讲到过,主节点上存在一个log dump线程会监听Bin-log日志,当日志出现变更时会通知从节点来拉取数据,而Canal的思想也是一样的,会监控主节点的Bin-log日志,当发生变更时,就直接去拉取数据,然后直接推送给从节点写入。

Canal在主从集群的身份就类似于一个中间商,对于主节点而言,它是一个从库,因为Canal会去主库上拉取新增数据。而对于集群中真正的从节点而言,它是一个主库,因为Canal会给其他真正的从节点推送数据。但这种方式也无法做到真正的数据实时性,毕竟Canal监听变更、拉取数据、推送数据都需要时间,这部分的时间开销必然存在,只是它会比从库去主库上拉取,速度会更快一些罢了。

一般企业内部都会引入Canal来解决数据不一致问题,因为它不仅仅只能解决主从延迟问题,还能解决MySQL-ES、MySQL-Redis.....等多种数据不一致的场景,Canal是由阿里巴巴开源的一项技术,也包括在阿里内部应用也较为广泛。

当然,也可以制定某些敏感数据走主库查询,这样能够确保数据的实时性,但容易模糊读写分离的界限,不过因为不需要引入额外的技术,所以在某些情况下也是个不错的方案。

五、总结

在本章中并未包含任何实操内容,更多是在讲述主从集群中数据同步复制的原理,以及详细讲解主从复制各方面的细枝末节知识,单独将这些理论性的内容拧出来说的原因,主要在于面试会经常问到,毕竟面试的过程中,面试官不可能问你主从集群该如何搭建,具体该写哪些参数等问题,所以这些理论性知识才是咱们真正应该学习的,这也为后续的《实践篇》奠定了扎实的理论基础~

同时,这章中所讲到的内容也额外重要,因为主从架构在Redis、ES、Zookeeper、MQ.....等各类技术栈中都存在,相互之间的思想也是共同的,当你彻底理解一个技术栈的主从架构后,其他技术栈中的主从思想亦是相同~

关于我
loading