深刻了解MySQL主从复制的原理

欢迎微信关注「SH的全栈笔记mysql

0. 主从复制

首先主从复制是什么?简单来讲是让一台MySQL服务器去复制另外一台MySQL的数据,使两个服务器的数据保持一致。web

这种方式与Redis的主从复制的思路没有太大的出入。若是你对Redis的主从复制感兴趣能够去看看《Redis的主从复制》。那既然Redis和MySQL都采用了复制这种方式,主从复制所带来的意义是什么呢?sql

经过复制功能,构建一个或者多个从库,能够提升数据库的高可用性可扩展性,同时实现负载均衡。当主库发生故障时,能够快速的切到其某一个从库,并将该从库提高为主库,由于数据都同样,因此不会影响系统的运行;当MySQL服务器须要扛住更多的读请求时,能够把读请求的流量分流到各个从库上去,写请求则转发给主库,造成读写分离的架构,来提供更好的读扩展和请求的负载均衡。数据库

读写分离的架构应用的其实很是普遍,就好比MySQL,还有Redis,以及咱们熟悉的Zookeeper,Zookeeper的Follower收到读请求不会本身处理,而是会将读请求转发给Leader,感兴趣的能够本身下来了解一下,这里就不偏题了。服务器

1. 复制原理

MySQL的主从复制支持两种方式:微信

  • 基于
  • 基于 语句

基于语句的复制在MySQL3.23中就已经有了,而基于语句的方式则在5.1中才实现。其本质都是基于主库的binlog来实现的,主库记录binlog,而后从库将binlog在本身的服务器上重放,从而保证了主、从的数据一致性。session

1.1 binlog

MySQL中日志分为两个维度,一个是MySQL服务器的,一个是底层存储引擎的。而上文提到的binlog就是属于MySQL服务器的日志,binlog也叫二进制日志,记录了全部对MySQL所作的更改。架构

基于行、语句的复制方式跟binlog的存储方式有关系。 binlog有三种存储格式,分别是Statement、Row和Mixed。并发

  • Statement 基于语句,只记录对数据作了修改的SQL语句,可以有效的减小binlog的数据量,提升读取、基于binlog重放的性能
  • Row 只记录被修改的行,因此Row记录的binlog日志量通常来讲会比Statement格式要多。基于Row的binlog日志很是完整、清晰,记录了全部数据的变更,可是缺点是可能会很是多,例如一条 update语句,有多是全部的数据都有修改;再例如 alter table之类的,修改了某个字段,一样的每条记录都有改动。
  • Mixed Statement和Row的结合,怎么个结合法呢。例如像 update或者 alter table之类的语句修改,采用Statement格式。其他的对数据的修改例如 updatedelete采用Row格式进行记录。

为何会有这么多方式呢?由于Statement只会记录SQL语句,可是并不能保证全部状况下这些语句在从库上可以正确的被重放出来。由于可能顺序不对。负载均衡

MySQL何时会记录binlog呢?是在事务提交的时候,并非按照语句的执行顺序来记录,当记录完binlog以后,就会通知底层的存储引擎提交事务,因此有可能由于语句顺序错误致使语句出错。

1.2 查看binlog

这里拿MySQL 5.6举例子,binlog默认是处于关闭状态的。咱们能够经过命令show variables like '%log_bin%' 来查看关于binlog的配置。

默认配置
默认配置

log_bin表明是否开启了binlog,其默认值为OFF

  • log_bin 表明是否开启了binlog,其默认值为 OFF
  • log_bin_basename binlog存储文件的完整名称,会在默认的文件名后面添加上 递增的序号,就例如 mysql-bin.000001
  • log_bin_index binlog索引文件名称,例如 mysql-bin.index
  • sql_log_bin 在binlog开启的时候,能够禁用当前session的binlog

你能够在MySQL中经过命令show binary logs查看全部的binlog文件

查看binlog
查看binlog

知道了有哪些文件以后咱们能够来看看binlog文件中的内容,能够在MySQL经过show binlog events命令来查看。

show binglog events 查看第一个binlog文件,咱们也能够经过in参数来指定,假设咱们想看的文件名是mysql-bin.000001,那么可使用命令show binlog events in 'mysql-bin.000001'来查看指定的binlog文件

查看binlog
查看binlog

接下来咱们来看看咱们在MySQL中的操做所对应的binlog内容分别是什么。

初始化

咱们上面提到过,binlog是由一个一个的event组成的。从MySQL 5.0开始,binlog的第一个event都为Format_desc,位于图中的Event_type那一列。能够看到内容为Server ver;5.6.50-log, Binlog ver: 4,说明当前使用的MySQL版本为5.6.50,Binlog的版本是V4。

建立数据库

而后我建立了一个名为student的DB,其Event_type是Query,这个event的内容为CREATE DATABASE student DEFAULT CHARACTER SET = utf8mb4,一个建库语句。

新建表

而后我建立了一个名为student的表,Event_type也是Query,内容为use student; CREATE TABLE student (id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT),一个建表语句。

插入数据

而后咱们执行INSERT语句给该表插入两行数据,再次查看binlog。

INSERT INTO `student` (`id``name`VALUES (NULL'张三');
INSERT INTO `student` (`id``name`VALUES (NULL'李四');
image-20210106123550397
image-20210106123550397

能够看到每次INSERT都会开启一个事务,你可能会疑惑,咱们只是简单的执行了INSERT语句,没有显示的开启事务。那为何会有事务产生呢?

这是由于MySQL采用了自动提交(AUTOCOMMIT)的机制,我使用的InnoDB存储引擎,是支持事务的,全部的用户活动都发生在事务中。咱们能够经过show variables like '%AUTOCOMMIT%';命令查看,若是结果是ON则表明是开启的。

1.3 复制的核心步骤

咱们假设主库已经开启了binlog,并正常的记录binlog。

首先从库启动I/O线程,跟主库创建客户端链接。

主库启动binlog dump线程,读取主库上的binlog event发送给从库的I/O线程,I/O线程获取到binlog event以后将其写入到本身的Relay Log中。

而后从库启动SQL线程,将Relay中的数据进行重放,完成从库的数据更新。

总结来讲,主库上只会有一个线程,而从库上则会有两个线程。

主从复制流程
主从复制流程

1.4 Relay Log

relay log其实和binlog没有太大的区别,在MySQL 4.0 以前是没有Relay Log这部分的,整个过程当中只有两个线程。可是这样也带来一个问题,那就是复制的过程须要同步的进行,很容易被影响,并且效率不高。例如主库必需要等待从库读取完了才能发送下一个binlog事件。这就有点相似于一个阻塞的信道和非阻塞的信道。

阻塞信道
阻塞信道

阻塞信道就跟你在柜台同样,你要递归柜员一个东西,可是你和柜员之间没有能够放东西的地方,你就只能一直把文件拿着,直到柜员接手;而非阻塞信道就像大家之间有个地方能够放文件,你就直接放上去就行了,不用等柜员接手。

引入了Relay Log以后,让本来同步的获取事件、重放事件解耦了,两个步骤能够异步的进行,Relay Log充当了缓冲区的做用。Relay Log有一个relay-log.info的文件,用于记录当前复制的进度,下一个事件从什么Pos开始写入,该文件由SQL线程负责更新。

1.5 Relay Log核心参数

接下来让咱们了解一下Relay Log的核心参数。

  • max_relay_log_size 中继日志的最大size,默认值0,若是为0就会取默认的size 1G,不然就为设置的值

  • relay_log 定义relay的名称,默认为主机名+relay-bin,例如像hostname-relay-bin

  • relay_log_basename 中继日志的全路径,即路径 + 文件名,例如/path/to/hostname-relay-bin,最大长度为256

  • relay_log_index 定义中继日志的索引文件的全路径,一样其最大的长度为256. 其默认值为hostname + relay-bin.index,例如/path/to/hostname-relay-bin.index

  • relay_log_info_file 定义relay-log.info文件的名称

  • relay_log_info_repository 存放relay log重放的数据的方式,能够设置为FILETABLE。FILE表明将中继日志重放的数据记录在relay-info.log中,TABLE则将其存放在slave_relay_log_info这张表里。

  • relay_log_purge 是否自动清空不须要的中继日志,默认值为ON

  • relay_log_recovery 当从库宕机后,若是relay log损坏了致使部分的中继日志没有进行同步,则自动放弃全部未进行重放的中继日志,并从主库从新获取,默认值为OFF

  • relay_log_space_limit 设置中继日志的最大值,防止写满磁盘。可是不建议设置这个值,建议仍是给中继日志须要的空间,0就是不限制,0也是默认值

  • sync_relay_log 用于控制中继日志写入磁盘的变量,假设值为n,那么在中继日志每接受n次binlog事件以后就会调用fdatasync()函数将中继日志强制的刷入磁盘;相反,若是值为0,则写入OS的缓冲区内,由OS调度决定什么时候将中继日志刷入磁盘,这样一来若是在没有刷入以前报错了,那么中继日志就会丢失。默认值是10000,也就是每向中继日志中写入1w次binlog事件就将中继日志强制的刷入磁盘。

  • sync_relay_log_info 该参数的影响跟参数relay_log_info_repository有必定关系,同时也跟是否使用支持事务的存储引擎有关系。该值默认也是10000.

    • sync_relay_log_info为0时

      • relay_log_info_repository为FILE,MySQL不会调用fdatasync(),而是将刷入磁盘的调度交给OS;
      • relay_log_info_repository为TABLE,若是使用了支持事务的存储引擎,则每次事务的时候该表都会被更新;若是没有使用事务引擎,则永远不会被更新
    • sync_relay_log_info大于0时

      • relay_log_info_repository为FILE,假设设置的值为N,那么 每N次事务都会都会调用fdatasync()强制将relay-log.info刷入磁盘
      • relay_log_info_repository为TABLE,若是使用了支持事务的引擎,则该表 每次事务结束都会被更新;若是没有使用事务引擎则会在 写入N个binlog事件的时候更新该表。

2. 复制模型

日常的开发中,其实不多说一上来就直接搞主从架构的。费时间、费钱还引入了额外的复杂度,最后发现投入了这么多一个单MySQL服务器就彻底能handle。

这就跟一个产品的架构迭代是同样的,刚刚起步的时候一个单体应用足够了。当你的业务扩展,请求膨胀,单体没法抗住压力了,就会考虑开始部署多实例,开始采用微服务架构去作横向扩展、负载均衡。

2.1 一主多从

固然你也能够把它当成一主一从

这是最简单的模型,特别适合少许写、大量读的状况。读请求被分到了各个从库上,有效的帮主库分散了压力,可以提高读并发。固然,你也能够只是把从库当成一个灾备库,除了主从复制以外,没有其余任何的请求和数据传输。

甚至你能够把其中一个备库做为你的预发环境的数据库,固然,这说到底仍是直接动了生产环境的数据库,是一种过于理想的用途,由于这还涉及到生产环境数据库的数据敏感性。不是全部人都可以接触到的,须要有完善的权限机制。

MySQL一主多从
MySQL一主多从

值得注意的是,若是有n个从库,那么主库上就会有n个binlog dump线程。若是这个n比较大的话在复制的时候可能会形成主库的性能抖动。因此在从库较多的状况下能够采用级联复制。

2.2 级联复制

级联复制用大白话说就是套娃

原本从库B、C、D、E、F、G都是复制的主库A,可是如今因为A的压力比较大,就不这么干了,调整成了以下的模式。

  • B、C复制A
  • D、E复制B
  • F、G复制C
MySQL级联复制
MySQL级联复制

这就叫级联复制,开启疯狂套娃模式。你甚至会以为这种套娃很眼熟,在Redis主从复制中也能够采用级联模式, slave去复制另外一个slave。

级联复制的好处在于很大程度上减轻了主库的压力,主库只须要关心与其有直接复制关系的从库,剩下的复制则交给从库便可。相反,因为是这种层层嵌套的关系,若是在较上层出现了错误,会影响到挂在该服务器下的全部子库,这些错误的影响效果被放大了。

2.3 主主复制

顾名思义,就是两个主库相互复制,客户端能够对任意一台主库进行写操做。任何一台主库服务器上的数据发生了变化都会同步到另外一台服务器上去。有点相似于Eureka Server的双节点模式,两个注册中心相互注册。这样一来,任何一台挂了都不会对系统产生影响。

并且主主复制能够打破数据库性能瓶颈,一个很酷的功能——横向扩展。为何说很酷呢,若是DB能作到横向扩展,那不少被数据库并发所限制的瓶颈均可以被突破,然而...

可是主主复制其实并不可靠,两边的数据冲突的可能性很大。例如复制中止了,系统仍然在向两个主库中写入数据,也就是说一部分数据在A,另外一部分的数据在B,可是没有相互复制,且数据也不一样步了。要修复这部分数据的难度就会变得至关大。

因此我认为双主的更多的意义在于HA,而不是负载均衡。

2.4 主、被动的主主复制

一样仍是双主的结构,可是区别在于其中一台是只读的被动服务器,客户端不会向该库进行写操做。

其用途在哪里呢?例如咱们要在不中断服务的前提下对MySQL进行维护、优化,举个例子——修改表结构。假设咱们有两个数据库,主库A和被动主库B,注意此处的被动主库是只读的,咱们先中止A对B的复制,也就是停掉A上的SQL线程。

主主中止复制
主主中止复制

这样一来,咱们以后在B上执行的很是耗时、可能须要锁表的操做就不会当即同步到A上来。由于此时A正在对外提供服务,因此不能使其收到影响,可是因为采用的是异步的复制模式,因此Relay Log仍是继续由I/O线程写入,只是不去进行重放。

而后咱们在B上执行这次的维护操做,注意,此时A上面发生的更新仍是会正常的同步到B来。执行完后交换读写的角色。也就是让A变成只读的被动主库,而B变为主动主库对外提供服务。

从新开启SQL线程
从新开启SQL线程

而后从新开启SQL线程,A开始去对以前Relay Log中积累的event进行重放。虽然A此时可能会阻塞住,可是A已经没有对外提供服务了,因此没有问题。

主、被动下的主主模式的好处你们也就清楚了,能够在不中止服务的状况下去作数据库的结构更新,其次能够在主库发生故障的状况下,快速的切换,保证数据库的HA。

3. 复制方式

上文咱们不止一次的提到了复制是异步的,接下来咱们来了解一下MySQL的主从复制都有哪些方式。

3.1 异步复制

首先就是异步,这也是MySQL默认的方式。在异步复制下,主库不会主动的向从库发送消息,而是等待从库的I/O线程创建链接,而后主库建立binlog dump线程,把binlog event发送给I/O线程,流程以下图。

MySQL复制模式
MySQL复制模式

主库在执行完本身的事务、记录完binlog以后就会直接返回,不会与客户端确认任何结果。而后后续由binlog dump线程异步的读取binlog,而后发送给从库。处理请求主从复制是两个彻底异步化的过程。

3.2 同步复制

同步模式则是,主库执行一个事务,那么主库必须等待全部的从库所有执行完事务返回commit以后才能给客户端返回成功,

同步复制
同步复制

值得注意的是,主库会直接提交事务,而不是等待全部从库返回以后再提交。MySQL只是延迟了对客户端的返回,并无延后事务的提交。

同步模式用脚趾头想知道性能会大打折扣,它把客户端的请求和主从复制耦合在了一块儿,若是有某个从库复制线程执行的慢,那么对客户端的响应也会慢不少。

3.3 半同步复制

半同步相对于同步的区别在于,同步须要等待全部的从库commit,而半同步只须要一个从库commit就能够返回了。若是超过默认的时间仍然没有从库commit,就会切换为异步模式再提交。客户端也不会一直去等待了。

MySQL复制模式
MySQL复制模式

由于即便后面主库宕机了,也能至少保证有一个从库节点是能够用的,此外还减小了同步时的等待时间。

4. 复制中的数据一致性

咱们在1.3中讨论了复制的核心步骤,看似很简单的一个流程,主库的binlog dump去读取binlog,而后从库的I/O线程去读取、写入Relay Log,进而从库的SQL线程再读取Relay Log进行重放。

那若是I/O线程复制到一半本身忽然挂掉了呢?又或者复制到一半主库宕机了呢?若是和保证数据一致性的呢?

咱们上面提到过,有一个relay-log.info的文件,用于记录当前从库正在复制的binlog和写入的Relay Log的Pos,只要这个文件还在,那么当从库意外重启以后,就会从新读取文件,从上次复制的地方开始继续复制。这就跟Redis中的主从复制相似,双方要维护一个offset,经过对比offset,来进行psync增量数据同步。

可是在MySQL 5.5以及以前,都只能将复制的进度记录在relog-log.info文件中。换句话说,参数relay_log_info_repository只支持FILE,能够再回到上面的1.5 Relay Log核心参数看一下。因此只有在sync_relay_log_info次事务以后才会把relay-log.info文件刷入磁盘。

若是在刷入磁盘以前从库挂了,那么重启以后就会发现SQL线程实际执行到位置和数据库记录的不一致,数据一致性的问题就这么产生了。

因此在MySQL 5.6时,参数relay_log_info_repository支持了TABLE,这样一来咱们就能够将复制的进度放在系统的mysql.slave_relay_log_info表里去,而且把更新进度、SQL线程执行用户事务绑定成一个事务执行。即便slave宕机了,咱们也能够经过MySQL内建的崩溃恢复机制来使实际执行的位置和数据库保存的进度恢复到一致。

其次还有上面提到的半同步复制,主库会先提交事务,而后等待从库的返回,再将结果返回给客户端,可是若是在主库等待的时候,从库挂了呢?

此时主库上因为事务已经提交了,可是从库上却没有这个数据。因此在MySQL 5.7时引入了无损半同步复制,增长了参数rpl_semi_sync_master_wait_point的值,在MySQL 5.7中值默认为after_sync,在MySQL 5.6中默认值为after_commit

  • after_sync 主库先不提交事务,等待某一个从库返回告终果以后,再提交事务。这样一来,若是从库在没有任何返回的状况下宕机了,master这边也没法提交事务。主从仍然是一致的
  • after_commit 与以前讨论的同样,主库先提交事务,等待从库返回结果再通知客户端

好了以上就是本篇博客的所有内容了,若是你以为这篇文章对你有帮助,还麻烦点个赞关个注分个享留个言

欢迎微信搜索关注【SH的全栈笔记】,查看更多相关文章

相关文章
相关标签/搜索