mysql主从复制原理解析

MySQL主从同步原理


MySQL主从同步原理

MySQL主从同步是在MySQL主从复制(Master-Slave Replication)基础上实现的,通过设置在Master MySQL上的binlog(使其处于打开状态),Slave MySQL上通过一个I/O线程从Master MySQL上读取binlog,然后传输到Slave MySQL的中继日志中,然后Slave MySQL的SQL线程从中继日志中读取中继日志,然后应用到Slave MySQL的数据库中。这样实现了主从数据同步功能。
主从原理图

0. 主从复制

首先主从复制是什么?

  • 简单来说是让一台MySQL服务器去复制另一台MySQL的数据,使两个服务器的数据保持一致。

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

读写分离的架构应用的其实非常广泛,就比如MySQL,还有Redis,以及我们熟悉的Zookeeper,Zookeeper的Follower收到读请求不会自己处理,而是会将读请求转发给Leader,感兴趣的可以自己下来了解一下,这里就不偏题了。

1. 复制原理

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

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

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格式。其余的对数据的修改例如update和delete采用Row格式进行记录。
为什么会有这么多方式呢?因为Statement只会记录SQL语句,但是并不能保证所有情况下这些语句在从库上能够正确的被重放出来。因为可能顺序不对。

MySQL什么时候会记录binlog呢?
是在事务提交的时候,并不是按照语句的执行顺序来记录,当记录完binlog之后,就会通知底层的存储引擎提交事务,所以有可能因为语句顺序错误导致语句出错。

1.2 查看binlog

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

show variables like '%log_bin%' 
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文件中的内容,可以在MySQL通过show binlog events命令来查看。
show binglog events 查看第一个binlog文件,我们也可以通过in参数来指定,假设我们想看的文件名是mysql-bin.000001,那么可以使用命令show binlog events in 'mysql-bin.000001'来查看指定的binlog文件
可以看到每次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中的数据进行重放,完成从库的数据更新。
总结来说,主库上只会有一个线程,而从库上则会有两个线程。

主库db的更新事件(update、insert、delete)被写到binlog
主库创建一个binlog dump thread,把binlog的内容发送到从库
从库启动并发起连接,连接到主库
从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log
从库启动之后,创建一个SQL线程,从relay log里面读取内容,从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db

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重放的数据的方式,可以设置为FILE和TABLE。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 一主多从

当然你也可以把它当成一主一从。

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

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

值得注意的是,如果有n个从库,那么主库上就会有n个binlog dump线程。如果这个n比较大的话在复制的时候可能会造成主库的性能抖动。所以在从库较多的情况下可以采用级联复制。

2.2 级联复制

本来从库B、C、D、E、F、G都是复制的主库A,但是现在由于A的压力比较大,就不这么干了,调整成了如下的模式。

B、C复制A
D、E复制B
F、G复制C

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

2.3 主主复制

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

而且主主复制可以打破数据库性能瓶颈,一个很酷的功能——横向扩展。为什么说很酷呢,如果DB能做到横向扩展,那很多被数据库并发所限制的瓶颈都可以被突破,然而...

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

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

2.4 主、被动的主主复制

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

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

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

3. 复制方式

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

3.1 异步复制

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

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

3.2 同步复制

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

值得注意的是,主库会直接提交事务,而不是等待所有从库返回之后再提交。MySQL只是延迟了对客户端的返回,并没有延后事务的提交。
同步模式用脚趾头想知道性能会大打折扣,它把客户端的请求和主从复制耦合在了一起,如果有某个从库复制线程执行的慢,那么对客户端的响应也会慢很多。

3.3 半同步复制

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

因为即使后面主库宕机了,也能至少保证有一个从库节点是可以用的,此外还减少了同步时的等待时间。

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 与之前讨论的一样,主库先提交事务,等待从库返回结果再通知客户端

5. 传统复制

6. GTID复制

MySQL主从复制(GTID模式)

GTID复制原理:

基于GTID的复制是MySQL 5.6后新增的复制方式.

  • GTID (global transaction identifier) 即全局事务ID, 保证了在每个在主库上提交的事务在集群中有一个唯一的ID.

  • 在原来基于日志的复制中, 从库需要告知主库要从哪个偏移量进行增量同步, 如果指定错误会造成数据的遗漏, 从而造成数据的不一致.

  • 而基于GTID的复制中, 从库会告知主库已经执行的事务的GTID的值, 然后主库会将所有未执行的事务的GTID的列表返回给从库. 并且可以保证同一个事务只在指定的从库执行一次.

  • GTID是由server_uuid和事物id组成,格式为:GTID=server_uuid:transaction_id。server_uuid是在数据库启动过程中自动生成,每台机器的server-uuid不一样。uuid存放在数据目录的auto.conf文件中,而transaction_id就是事务提交时系统顺序分配的一个不会重复的序列号。
    查看server_uuid

SELECT @@server_uuid;

注意:server_uuid是mysql的识别码,server_id是在集群里面的id号,防止server_id冲突的

GTID的好处:

(1)GTID使用master_auto_position=1代替了binlog和position号的主从复制搭建方式,相比binlog和position方式更容易搭建主从复制。
(2)GTID方便实现主从之间的failover,不用一步一步的去查找position和binlog文件。

GTID模式复制搭建过程中注意事项:

主从需要设置如下参数(一般直接在配置文件/etc/my.cnf下直接添加):
a、主库配置:

gtid_mode=on
enforce_gtid_consistency=on
log_bin=on
server-id=33062200(主从不能相同)
binlog_format=row

b、从库配置:

gtid_mode=on
enforce_gtid_consistency=on
log_slave_updates=1
server-id=33062211(主从不能相同)

如果出现如下错误:

ERROR 1840 (HY000) at line 24: @@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_EXECUTED is empty.

则在从库命令行下执行:reset master

root@db 15:20:  [mysql]> reset master;

跟普通模式一样,执行change master语句,只不过其中的binlog和position换成master_auto_position=1

CHANGE MASTER TO
MASTER_HOST='192.168.112.220',
MASTER_USER='repluser',
MASTER_PASSWORD='repluser123',
MASTER_PORT=3306,
MASTER_AUTO_POSITION=1;

查看执行过的GTID

 show master status;

mysql.gtid_executed表

在MySQL5.7版本之后,gtid_executed这个值持久化了,在MySQL库下新增了一张gtid_executed

select * from mysql.gtid_executed;
+--------------------------------------+----------------+--------------+
| source_uuid                          | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| 362afb3d-e619-11ea-9b0c-fc15b4105a78 |              1 |     42474676 |
| 36c0b47a-e618-11ea-a7b3-d89d6763e0d1 |              1 |            5 |
| decbd4f5-e14c-11ea-aeb4-fc15b4100acc |              1 |            8 |

表记录的信息:
GTID 存储在 mysql 数据库中名为 gtid_executed 的表中。
此表中的一行对应它代表的每个GTID 或一组 GTID,包含原始服务器的 UUID(也就是配置过GTID复制的服务器),以及集合的开始和结束事务ID; 对于仅引用单个 GTID 的行,最后两个
值是相同的。

比如上面的信息就可以对应到如下命令中的Executed_Gtid_Set信息(表示一斤执行过的GTID信息)

mysql> show master status \G
*************************** 1. row ***************************
             File: mysql-bin.002190
         Position: 542427040
     Binlog_Do_DB: 
 Binlog_Ignore_DB: 
Executed_Gtid_Set: decbd4f5-e14c-11ea-aeb4-fc15b4100acc:1-31683285,
36c0b47a-e618-11ea-a7b3-d89d6763e0d1:1-279988634:279988636,
decbd4f5-e14c-11ea-aeb4-fc15b4100acc:1-8

show slave status \G

由上述内容可以看出:decbd4f5-e14c-11ea-aeb4-fc15b4100acc:1-8 是完全停止了事务执行的,通常我们在配置多源复制的时候就是通过指定多个GTID来实现的。

如下的命令可以查询出mysql已经执行过的GTID并且这部分已经可以删除掉。

select @@GLOBAL.gtid_purged\G

好处

  1. 该表会记录执行的GTID集合信息,因为有了该表,就不用再像MySQL 5.6版本时,必须开启log_slave_updates参数,从库才可以进行赋值。

  2. GTID信息会保存在gtid_executed表中,可以关闭从库的binlog,节约binlog的记录开销。在执行reset master时,会清空表内所有的数据。

  3. 而MySQL 5.7还有一个参数gtid_executed_compresssion_period参数,用来控制gtid_executed表的压缩,该参数默认值为1000,意思是表压缩在执行完1000个事务之后开始。

show variables like '%gtid_executed%';

从MySQL5.7.6开始,gid_mode支持动态修改,gtid_mode可取值为:
OFF-------不支持GTID事务
OFF_PERMISSIVE----新的事务是匿名的,同时允许复制的事务可以是GTID,也可以是匿名的
ON_PERMISSIVE---- 新的事务使用GTID,同时允许复制的事务可以是GTID,也可以是匿名的
ON------支持GITD的事务

在线上环境中,有可能把传统复制改为GTID的复制模式的需求,这里特意强调一点,gtid_mode虽然支持动态修改,但不支持跳跃式修改。从ON_PERMISSIVE直接修改为OFF是不可以的,我们可以通过实验来展示传统复制与GTID复制之间的切换过程。见(《MySQL主从复制之传统复制与GTID模式之间切换》)

GTID模式复制局限性:

(1)不能使用create table table_name  select * from table_name模式的语句
(2)在一个事务中既包含事务表的操作又包含非事务表
(3)不支持CREATE TEMPORARY TABLE  or DROP TEMPORARY TABLE语句操作
(4)使用GTID复制从库跳过错误时,不支持sql_slave_skip_counter参数的语法

1、不支持非事务引擎(从库报错,stopslave; start slave; 忽略)
2、不支持 create table … select 语句复制(主库直接报错)
3、不允许在一个 SQL 同时更新一个事务引擎和非事务引擎的表
4、在一个复制组中,必须要求统一开启 GTID 或是关闭 GTID
5、开启 GTID 需要重启(5.7 中不需要)
6、开启 GTID 后,就不在使用原来的传统的复制方式
7、对于 createtemporary table 和 drop temporary table 语句不支持
8、不支持 sql_slave_skip_counter
posted @ 2025-07-07 11:38  数据库小白(专注)  阅读(32)  评论(0)    收藏  举报