当前位置: 首页 > 科技观察

深入理解MySQL主从复制原理

时间:2023-03-20 18:27:40 科技观察

本文转载自微信公众号《SH的全栈笔记》,作者SH。转载本文请联系SH全栈笔记公众号。SH0。主从复制首先,什么是主从复制?简单的说就是让一台MySQL服务器复制另一台MySQL的数据,让两台服务器的数据保持一致。这种方式和Redis的主从复制思路没有太大区别。如果你对Redis主从复制感兴趣,可以查看《Redis的主从复制》。既然Redis和MySQL都采用了复制的方式,那主从复制的意义何在呢?通过复制功能,构建一个或多个从库,可以提高数据库的高可用性和可扩展性,同时实现负载均衡。当主库出现故障时,可以快速切换到它的其中一个从库,并将从库提升为主库,因为数据是一样的,所以不会影响系统的运行;当MySQL服务器需要处理更多的读请求时,可以将读请求的流量分散到各个从库,写请求转发到主库,形成读写分离架构,提供更好的读扩展和请求负载均衡。读写分离的架构其实应用非常广泛,比如MySQL、Redis,还有我们熟悉的Zookeeper。Zookeeper的Follower不会自己处理读请求,而是将读请求转发给Leader。有兴趣的可以自己下来了解一下,这里就不跑题了。一、复制原理MySQL的主从复制支持两种方式:row-basedstatement-basedstatement-basedreplication在MySQL3.23中已经存在,而statement-based方式在5.1才实现。它的本质是基于主库的binlog实现的。主库记录binlog,然后从库在自己的服务器上重放binlog,从而保证主从之间的数据一致性。1.1binlogMySQL中的日志分为两个维度,一个是MySQL服务器端,一个是底层存储引擎。上面说的binlog是属于MySQL服务器的日志。binlog也叫binarylog,记录了对MySQL的所有修改。基于行和语句的复制方式与binlog的存储方式有关。Binlog有三种存储格式,分别是Statement、Row和Mixed。Statement基于语句,只记录修改了数据的SQL语句,可以有效减少binlog数据量,提高基于binlog的读取和回放性能。Row只记录修改过的行,所以Row记录的binlog日志量是平均的。它将超过Statement格式。Row-basedbinlog日志非常完整清晰,记录了所有的数据变化,但缺点是可能有很多条,比如一条update语句,所有的数据都可能被修改;再比如altertable等,修改了A字段,同样每条记录都有变化。MixedStatement和Row的组合是什么?例如update、altertable等语句修改,就采用Statement格式。其他对数据的修改,如update、delete,都记录在Row格式中。为什么有这么多方法?因为Statement只记录SQL语句,但不保证这些语句在所有情况下都能在从库上正确重放。因为它可能顺序错误。MySQL什么时候记录binlog?是事务提交的时候,不是按照语句的执行顺序。binlog记录下来后,会通知底层存储引擎提交事务,所以有可能是语句顺序错误导致的语句。错误。1.2查看binlog这里以MySQL5.6为例,binlog默认是关闭的。我们可以通过命令showvariableslike'%log_bin%'查看binlog的配置。默认配置log_bin表示是否启用binlog,其默认值为OFF。log_bin代表是否启用binlog,默认值为OFF。log_bin_basename是binlog存储文件的全名,默认文件名后会加一个递增序号,如mysql-bin.000001log_bin_indexbinlog索引文件名,如mysql-bin.indexsql_log_bin时binlog开启后,可以关闭当前session的binlog。在MySQL中通过命令showbinarylogs可以查看所有的binlog文件图片。知道了binlog中有哪些文件之后,我们就可以看一下binlog文件的内容了。可以通过MySQL中的showbinlogevents命令查看。showbinglogevents查看第一个binlog文件,我们也可以通过in参数指定,假设我们要查看的文件名为mysql-bin.000001,那么可以使用命令showbinlogeventsin'mysql-bin.000001'toviewSpecifiedbinlogfile查看binlog接下来我们看一下我们在MySQL中的操作对应的binlog的内容。初始化上面我们说了,binlog是由event一个一个的组成的。从MySQL5.0开始,binlog的第一个事件是Format_desc,位于图中的Event_type列。可以看到内容为Serverver;5.6.50-log,Binlogver:4,说明当前MySQL版本为5.6.50,Binlog版本为V4。创建数据库然后我创建了一个名为student的DB,它的Event_type是Query,这个事件的内容是CREATEDATABASEstudentDEFAULTCHARACTERSET=utf8mb4,一条建库语句。新建一张表然后我新建了一张表,名字叫student,Event_type也是Query,内容是usestudent;CREATETABLEstudent(idINT(11)UNSIGNEDNOTNULLPRIMARYKEYAUTO_INCREMENT),一条建表语句。插入数据然后我们执行INSERT语句向表中插入两行数据,再次查看binlog。INSERTINTO`student`(`id`,`name`)VALUES(NULL,'张三');INSERTINTO`student`(`id`,`name`)VALUES(NULL,'李四');image-20210106123550397即可看到每次INSERT都会开启一个事务,你可能会疑惑,我们只是简单的执行了INSERT语句,并没有显示开启的事务。那为什么会发生交易呢?这是因为MySQL采用了自动提交(AUTOCOMMIT)机制。我使用的InnoDB存储引擎支持事务,所有的用户活动都发生在事务中。我们可以使用像'%AUTOCOMMIT%'这样的显示变量;命令查看,如果结果为ON,则表示已启用。1.3复制的核心步骤我们假设主库开启了binlog,并正常记录了binlog。首先从库启动I/O线程,与主库建立客户端连接。主库启动binlogdump线程,读取主库上的binlogevent发送给从库的I/O线程。I/O线程拿到binlog事件后,写入自己的RelayLog中。然后启动从库的SQL线程,重放Relay中的数据,完成从库的数据更新。综上所述,主库上只会有一个线程,从库上有两个线程。主从复制过程1.4RelayLogRelaylog与binlog没有太大区别。在MySQL4.0之前,没有RelayLog。整个过程只有两个线程。但是这也带来了一个问题,就是复制过程需要同步进行,容易受到影响,效率不高。比如主库必须等待从库读完,才能发送下一个binlogevent。这有点类似于阻塞通道和非阻塞通道。阻塞通道阻塞通道就像在柜台前一样。你想递归地告诉出纳员一些事情,但是你和出纳员之间没有地方放东西,所以你只能持有文件直到出纳员接手;不用堵通道,你可以,比如有放证件的地方,直接放那儿,不用等柜员接手。引入RelayLog后,将原来的同步获取和重放事件解耦,两个步骤可以异步进行,RelayLog起到缓冲的作用。RelayLog有一个relay-log.info文件,用来记录当前复制的进度和下一次事件写入的Pos。该文件由SQL线程更新。1.5RelayLog的核心参数接下来,让我们了解一下RelayLog的核心参数。max_relay_log_size中继日志的最大大小,默认值为0,如果为0则取默认大小1G,否则为设置值relay_log定义中继的名称,默认为hostname+relay-bin,例如hostname-relay-binrelay_log_basenamerelaylog的完整路径,即路径+文件名,如/path/to/hostname-relay-bin,最大长度为256relay_log_index定义的完整路径中继日志的索引文件,最大长度为256。它的默认值是hostname+relay-bin.index,比如/path/to/hostname-relay-bin.indexrelay_log_info_file定义了relay-log.info文件的名字relay_log_info_repository可以设置relaylogreplay数据的存储方式到文件和表。FILE表示在relay-info.log中记录relaylog的重放数据,TABLE表示存放在slave_relay_log_info表中。relay_log_purge是否自动清除不需要的中继日志,默认值为ONrelay_log_recovery从库宕机时,如果中继日志损坏,部分中继日志未同步,所有未重放的中继日志将被自动丢弃,以及从主库重新获取,默认值为OFFrelay_log_space_limit设置relaylog的最大值,防止磁盘被占满。但是,不建议设置此值。建议给中继日志需要的空间。0表示没有限制,0也是默认值。sync_relay_log用于控制中继日志写入磁盘的变量。收到n个binlog事件后,会调用fdatasync()函数强制将relaylog刷到磁盘;反之,如果值为0,则写入OS的buffer,由OS调度决定何时将relaylog刷入磁盘,这样如果在没有刷入之前报错,中继日志将丢失。默认值为10000,即每向relaylog写入1w个binlogevents,relaylog就会强制刷盘。sync_relay_log_info该参数的影响与参数relay_log_info_repository有一定的关系,也与是否使用支持事务的存储引擎有关。默认情况下,该值也是10000。relay_log_info_repository为FILE,假设设置值为N,则每N个事务都会调用fdatasync()强制将relay-log.info刷入磁盘。relay_log_info_repository是TABLE,如果使用支持事务的引擎,那么每次事务结束都会更新该表;如果不使用事务引擎,则在写入N个binlog事件时更新表。relay_log_info_repositoryisFILE,MySQL不会调用fdatasync(),而是将flushingtodisk的调度交给OS;relay_log_info_repository是TABLE,如果使用支持事务的存储引擎,每次事务都会更新该表;如果不使用事务引擎,则永远不会更新。当sync_relay_log_info为0时,当sync_relay_log_info大于0时。2.复制模型在平时的开发中,很少说一开始就直接实现master-slave架构。这既费时又费钱,并且带来了额外的复杂性。投入了这么多,终于发现单台MySQL服务器完全可以搞定。这与产品的架构迭代相同。一开始一个应用程序就足够了。当你的业务扩展,请求扩展,单体无法承受压力时,你会考虑部署多实例,开始使用微服务架构进行水平扩展和负载均衡。2.1一主多从当然,你也可以看成一主一从。这是最简单的模型,特别适合少量写入大量读取的情况。读请求分配到各个从库,有效帮助主库分散压力,提高读并发度。当然,你也可以只把从库当成容灾库,除了主从复制之外没有其他的请求和数据传输。您甚至可以将其中一个备用数据库用作预发布环境的数据库。当然,这归根结底直接影响到生产环境的数据库,是比较理想的使用方式,因为这也涉及到生产环境的数据库的数据敏感性。性别。不是所有人都可以访问,需要完善的权限机制。MySQL有一个master和多个slave。值得注意的是,如果有n个从库,那么主库上就会有n个binlogdump线程。如果这个n比较大,在复制的时候,主库的性能可能会出现波动。所以当从库很多的时候可以使用级联复制。2.2级联复制级联复制用大白话来说就是套娃。本来从库B、C、D、E、F、G都是从主库A复制过来的,现在由于A的压力比较大,所以没有这样做,调整如下模式。B、C复制AD、E复制BF、G复制CMySQL级联复制这个叫级联复制,开启疯狂娃娃模式。你甚至会觉得这个套娃很眼熟。在Redis主从复制中,也可以采用级联模式,一个slave复制另一个slave。级联复制的好处是大大减轻了主库的压力。主库只需要关心与其有直接复制关系的从库,其余的复制交给从库。反之,由于层层嵌套的关系,如果在上层出现错误,会影响挂在服务器下的所有子库,这些错误的影响会被放大。2.3Master-MasterReplication顾名思义,两个master数据库相互复制,客户端可以向其中任一master数据库写入数据。主库服务器上的任何数据更改都将同步到另一台服务器。有点类似于EurekaServer的双节点模式,两个注册中心互相注册。这样,任何一个挂了,都不会影响系统。而master-master复制可以打破数据库性能的瓶颈,一个很酷的特性——水平扩展。为什么很酷?如果DB能够横向扩展,很多受数据库并发限制的瓶颈就可以突破。但是……但是master-master复制其实并不靠谱,两边数据冲突的可能性很大。例如,当复制停止时,系统还在向两个主库写数据,也就是说一部分数据在A,另一部分在B,但它们之间没有相互复制,数据不同步。修复这部分数据会变得相当困难。所以我觉得双主的意义在于HA而不是负载均衡。2.4主被动master-master复制也是双主结构,但不同的是其中一个是只读的passiveserver,client不会写库。它的用途在哪里?例如,我们需要在不中断服务的情况下维护和优化MySQL。比如修改表结构。假设我们有两个数据库,主库A和被动主库B。注意这里的被动主库是只读的。我们先停止A到B的复制,也就是停止A上的??SQL线程,master和master停止复制。这样,我们在B上执行的那些可能需要表锁的非常耗时的操作,就不会立即同步到A上了。因为此时A正在对外提供服务,所以不能受到影响。但是由于采用了异步复制方式,RelayLog会被I/O线程继续写入,但不会被重放。然后我们在B上进行这个维护操作,注意A上的更新还是会正常同步到B上。执行完成后,交换读和写的角色。即让A成为只读被动主库,B成为主动主库对外提供服务。重启SQL线程再重启SQL线程,A开始重放之前RelayLog中积累的事件。虽然此时A可能被屏蔽了,但是A一直没有对外提供服务,所以没有问题。主动和被动主-主模式的好处大家都很清楚。您可以在不停止服务的情况下更新数据库结构。其次,可以在主库出现故障时快速切换,保证数据库的HA。3.复制方式我们不止一次提到复制是异步的。接下来我们就来看看MySQL的主从复制的方法。3.1异步复制首先是异步的,这也是MySQL默认的方式。异步复制下,主库不会主动向从库发送消息,而是等待从库的I/O线程建立连接,然后主库创建binlogdump线程,将binlogevent发送到I/O线程。过程如下图。MySQL复制模式的主库在执行完自己的事务并记录binlog后会直接返回,不会和客户端确认任何结果。然后binlogdump线程异步读取binlog,然后发送到从库。处理请求和主从复制是两个完全异步的过程。3.2同步复制同步模式是主库执行一个事务,那么主库必须等待所有从库都执行完事务并返回commit,然后才返回成功给客户端。值得注意的是,主库会直接提交事务,而不是等所有从库都返回后再提交。MySQL只是延迟了对客户端的返回,并没有延迟事务的提交。同步模式用脚趾头就知道性能会大打折扣。它将客户端的请求与主从复制耦合起来。如果有执行慢的从库复制线程,对客户端的响应就会慢很多。3.3半同步复制半同步和同步的区别在于,同步需要等待所有的从库提交,而半同步只需要一次从库提交就可以返回。如果过了默认时间还没有从库提交,就会切换到异步模式重新提交。客户不必永远等待。MySQL复制模式可以保证即使后期主库宕机也至少有一个从库节点可用,同时也减少了同步时的等待时间。4.复制中的数据一致性。我们在1.3中讨论了复制的核心步骤。这似乎是一个非常简单的过程。主库的binlogdump读取binlog,然后从库的I/O线程读写。进入RelayLog,然后从库的SQL线程中读取RelayLog进行重放。那么如果I/O线程复制到一半突然挂掉了怎么办?还是一半的主库宕机了?如果保证数据一致性呢?上面我们提到,有一个relay-log.info文件,用来记录当前正在从库复制的binlog和写入RelayLog的Pos。只要这个文件还在,当从库意外重启的时候,就会重新读取这个文件,从上次复制的地方开始。继续复制。这类似于Redis中的主从复制。双方都需要维护一个偏移量,通过比较偏移量进行psync增量数据同步。但是在MySQL5.5及之前的版本中,复制的进度只能记录在relog-log.info文件中。也就是说relay_log_info_repository这个参数只支持FILE,大家可以回到上面1.5RelayLog核心参数看看。因此relay-log.info文件将仅在sync_relay_log_info事务之后刷新到磁盘。如果在刷入磁盘之前从库中挂掉,那么重启之后,你会发现SQL线程的实际执行位置和数据库记录不一致,数据一致性的问题就会由此产生。所以在MySQL5.6中,参数relay_log_info_repository支持TABLE,这样我们就可以把复制进度放到系统的mysql.slave_relay_log_info表中,将用户事务的更新进度和SQL线程执行绑定到一个事务执行中。即使slave宕机了,我们也可以利用MySQL内置的崩溃恢复机制,将实际执行位置和数据库中保存的进度恢复一致。其次,就是上面提到的半同步复制。主库会先提交事务,然后等待从库返回,再返回结果给客户端,但是万一主库在等待的时候从库挂了怎么办?事务已经在主库上提交,但是从库没有这个数据。因此在MySQL5.7中引入了无损半同步复制,并增加了参数rpl_semi_sync_master_wait_point的值。MySQL5.7中默认值为after_sync,MySQL5.6中默认值为after_commit。after_sync主库不先提交事务,等待某个从库返回结果后再提交事务。这样,如果从库宕机没有任何回报,主库就无法提交事务。主从还是一致的。after_commit与之前讨论的相同。主库先提交事务,等待从库返回结果再通知客户端