随着项目架构的不断扩大,单台 MySQL 已经不能满足需要了,所以需要搭建集群将前来的请求进行分流处理。博客主要根据丁奇老师的专栏<<MySQL实战45讲>>学习的总结。
架构
MySQL的集群和 Redis 集群类似,都是默认为master 库,可以设置为从库,主库负责处理写请求,从库处理读请求。一般将从库设置为 read-only,也就是将这个参数设为 true。这样既避免了在主从切换、测试时从库的误操作导致主从不一致,同时也可以通过这个属性来判断当前库是什么角色。
种类
1、按 master 数量来看,架构可以分为两种。
第一种是明确的主备关系,这种使用的不多,因为在主备切换时会比较耗时。
第二种是有多个master,master 之间互为主备关系,只不过保留一个处理写操作,其他的设置 readonly=true,只处理读请求。这种架构在切换时比较方便、快捷。
2、按是否代理的角度来看,也可以分为两种。
第一种是不使用代理。这种方式的好处是架构简单、执行快、排错快。缺点是在主备切换、库迁移时需要改变后台数据库连接信息。
第二种是使用代理。这样做的好处是后端不需要关注数据库操作的细节,连接维护、后台信息维护都是由 proxy 完成的。但缺点也很明显,这样的架构比较复杂,配置、维护起来比较吃力,且 proxy 必须是高可用的。
搭建
1、配置master:
1)打开配置文件(默认在/usr/my.cnf),主要配置的参数:
server-id:当前库的 id
log-bin:binlog 存储位置
read-only:是否只读。1代表只读,0代表读写。
binlog-ignore-db:不保存操作到binlog 的数据库,可以设为 mysql
然后重启:service mysql restart ;
2)创建同步数据的账户,并授权:
grant replication slave on *.* to ‘用户名‘@‘‘ip地址‘ identified by ‘密码‘ ;
flush privilages;
3)查看 master 状态:show master status。得到 Position 值(日志最新写入点)。
2、配置 slave:
1)打开配置文件my.cnf,配置参数 server-id 、log-bin、read-only。然后重启。
2)配置从库与主库的关联:
CHANGE MASTER TO MASTER_HOST=$host_name MASTER_PORT=$port MASTER_USER=$user_name MASTER_PASSWORD=$password MASTER_LOG_FILE=$master_log_name MASTER_LOG_POS=$master_log_pos
MASTER_HOST、MASTER_PORT、MASTER_USER 和 MASTER_PASSWORD 四个参数,分别代表了主库 A’的 IP、端口、用户名和密码。最后两个参数 MASTER_LOG_FILE 和 MASTER_LOG_POS 表示,要从主库的 master_log_name 文件的 master_log_pos 这个位置的日志继续同步。而这个值就是前面读取的 Position 值。
上面这种是5.6之前通过位点来实现同步的,其存在很多不足,比如这个位点在主库切换时需要重新去获取,并且在新的主库启动后从库可能会导致操作重复执行导致抛出异常,所以在5.6引入了 GTID来替换位点,解决了这个问题。具体在下文会解释。这里先说一下使用 GTID 来完成主备连接。
1)首先要在主从库上都设置参数:SET GLOBAL ENFORCE_GTID_CONSISTENCY = ‘ON‘; SET GLOBAL GTID_MODE = ‘ON‘; 如果要永久有效,在配置文件 my.cnf 中配置 gtid-mode=ON enforce-gtid-consistency=1 。
2)配置从库与主库的关联:
CHANGE MASTER TO MASTER_HOST=$host_name MASTER_PORT=$port MASTER_USER=$user_name MASTER_PASSWORD=$password master_auto_position=1 。
最后开启同步:start slave;
原理
内部执行图:
1、在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
2、在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。
3、主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog(先从page cache中读取,没有再读取磁盘),发给 B。
4、备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
5、sql_thread 读取中转日志,解析出日志里的命令,并执行。
循环复制
这里从库可以将从主库传来的更新的数据也进行持久化记录在本地的 binlog 中,通过将参数 log_slave_updates 设为 on 来开启这个功能。但是这样可能会发生循环复制。
更新操作的 binlog 的执行机制:某个库处理了写操作,那么它就会将这个写操作相关的 binlog 发送给其他与其关联的库,这个 binlog 上会记录当前库的 server-id,其他库收到 binlog 后会判断 server-id 是否是当前库的 server-id,如果不是就应用到库中并记录到 binlog(这里记录不会改变原有的接收到的数据,也就不是修改 server-id),然后再重复将写操作的 binlog 发送给其他库。
发生循环复制的场景:
1、主库执行写操作后,修改了当前库的 server-id。因为修改了server-id,所以主库在受到从库发送回来的日志后还会继续执行,执行完后还会重复发给从库,造成循环。
解决:这种只能提前规定库的 server-id 在运行时不能修改。
2、三节点中某个节点执行写操作后将写操作传给其他两个节点后另外两个节点发生了循环复制。
解决:先停止日志的发送,一段时间后再改回来。
stop slave; CHANGE MASTER TO IGNORE_SERVER_IDS=(server_id_of_B); start slave;
主从切换
主从切换指的就是主库与备库身份的切换。
切换策略
可靠性优先策略(优先)
可靠性优先策略是将数据的可靠性设为优先进行切换的策略。其核心就是先关闭主库A对写请求的处理,然后等待备库的数据延迟变为0,也就是备库B读取完从主库A传来的所有操作日志且全部落盘。这时再将备库B设为新的主库B。
1、判断备库 B 现在的 seconds_behind_master(通过 show slave status 查看),如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步;
2、把主库 A 改成只读状态,即把 readonly 设置为 true;
3、判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止;(最耗时)
4、把备库 B 改成可读写状态,也就是把 readonly 设置为 false;
5、把业务请求切到备库 B。
因为是可靠性优先,所以用这种方式切换后读取的数据是可以保证准确性的。缺点是因为在开始需要等待备库B与主库A的延迟变为0后 B库才可以设为主库,而在这等待的过程中前来的写请求是无法被处理的,全部会被阻塞,如果并发的写操作很多,那么就会很影响系统的响应性能。
可用性优先策略
可用性优先策略是将上面可靠性优先策略步骤里的4,5提前到最开始执行,这样就不会出现写操作被阻塞的情况了,保证了可用性。但是这样带来的缺点是很严重的。可用性优先策略执行过程会根据 binlog 格式的不同有所不同。
1、mixed 格式
主键是自增的。执行过程:
1、步骤 2 中,主库 A 执行完 insert 语句,插入了一行数据(4,4),之后开始进行主备切换。
2、步骤 3 中,由于主备之间有 5 秒的延迟,所以备库 B 还没来得及应用“插入 c=4”这个中转日志,就开始接收客户端“插入 c=5”的命令。
3、步骤 4 中,备库 B 插入了一行数据(4,5),并且把这个 binlog 发给主库 A。
4、步骤 5 中,备库 B 执行“插入 c=4”这个中转日志,插入了一行数据(5,4)。而直接在备库 B 执行的“插入 c=5”这个语句,传到主库 A,就插入了一行新数据(5,5)。
最终导致主从数据不一致,同时在查询相应数据时会返回错误的数据,并且还无法被发现,这是非常致命的。
2、Row 格式
在使用 row 格式的 binlog 进行日志记录,那么记录的是完整的操作数据,所以不会出现上面出现的情况,并且在发生这种异常时会抛出异常提示。
总结
因为可用性优先策略很容易造成数据不一致,所以 一般使用的都是可靠性优先策略,但是因为可靠性策略在等待备库赶上主库时写操作会被阻塞,所以主从延迟就决定了 MySQL 的可用性。主从延迟越低,主库在异常宕机后,从机就越快赶上主库的数据,更快恢复。
主从同步方式
这里以一主多从的架构为例说明。
假设原本 A 是主机,A‘ 和 A 互为主备关系,只不过 A‘ read-only 设为 true,B、C、D都是A的从库。这时A突然宕机,那么想要将 A‘ 设为新的主机,并且将 B、C、D 的从属关系变为 A‘。
通过日志的同步位点(传统方式)
在上面搭建时在配置从库与主库的关联时说到有两种方式,第一种就是通过日志位点来确定从库开始接收日志的起始点,这种方式在初始配置时可以直接查看并赋值,但是如果中间主机宕机或者主动切换时就比较麻烦了。所以此时需要做的是:
1、等待新主库 A’把中转日志(relay log)全部同步完成;
2、在 A’上执行 show master status 命令,得到当前 A’上最新的 File 和 Position;
3、取原主库 A 故障的时刻 T;
4、用 mysqlbinlog 工具解析 A’的 File,得到 T 时刻的位点。mysqlbinlog File --stop-datetime=T --start-datetime=T
得到 123,将这个值作为位点来设置B、C、D 的从属关系。
位点不准确:通过上面方法得到的值并一定是准确的,如果主库 A 在宕机前刚执行了一条insert事务,并且将此事务发给了 A‘、B,传完后立刻宕机,那么 A‘、B 都已经同步了这一行 insert 事务,这时将 A‘ 作为主机启动,然后在将 B 的从属关系改成 A‘ 时把 Position 还写成读取到的 insert 事务那一行,那么就会重复执行,如果格式是 row 的话就会抛出异常(statement 就会造成主从不一致)。
如果是抛出异常,就会断开与主库的连接,所以需要我们去处理,恢复连接。
解决方式:
1、抛出异常后跳过。
跳过:set global sql_slave_skip_counter=1;
开启从库:start slave;
2、跳过指定的错误(不推荐)。
上面重复执行遇到的错误主要是两种:1062,插入数据时唯一键不唯一;1032,删除数据时找不到行。
所以可以将 slave_skip_errors 设为 "1032、1062"。
通过这种方式如果后端传来的请求也会造成这样的错误则也会被跳过,所以可能会引起后端无法收到反馈。这种方式只适用于无法确定同步点且确定到从库恢复这段时间不会有 1032、1062 的事务,并且在一段时间后还需要将这个参数修改回去。
通过GTID(推荐)
是什么:GTID 是 MySQL5.6 引入的概念,表示的是全局事务ID,是一个事务的唯一标识,由两部分组成,格式是:
官方文档是:GTID=source_id:transaction_id
便于理解可以看作:GTID=server_uuid:gno
server_uuid 是一个实例第一次启动时自动生成的,是一个全局唯一的值;
gno 是一个整数,初始值是 1,每次提交事务的时候分配给这个事务,并加 1。
transaction_id 是事务id,但是事务回滚的话,这个值也会自增。
使用:数据库在启动时加上参数 gtid_mode=on 和 enforce_gtid_consistency=on 来开启 GTID,如果要永久有效,在配置文件 my.cnf 中配置 gtid-mode=ON enforce-gtid-consistency=1 。
在主机A 宕机后,改变从库 B、C、D 的 slave 关系时使用
CHANGE MASTER TO MASTER_HOST=$host_name MASTER_PORT=$port MASTER_USER=$user_name MASTER_PASSWORD=$password master_auto_position=1
切换原理:
1、将实例A‘的 GTID 集合记为 set_a,实例B 的 GTID 集合记为 set_b。那么执行逻辑如下:
2、实例 B 指定主库 A’,基于主备协议建立连接。
3、实例 B 把 set_b 发给主库 A’。
4、实例 A’算出 set_a 与 set_b 的差集,也就是所有存在于 set_a,但是不存在于 set_b 的 GTID 的集合,判断 A’本地是否包含了这个差集需要的所有 binlog 事务。
a. 如果不包含,表示 A’已经把实例 B 需要的 binlog 给删掉了,直接返回错误;
b. 如果确认全部包含,A’从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,发给 B;
5、之后就从这个事务开始,往后读文件,按顺序取 binlog 发给 B 去执行。
GTID生成方式:
1、gtid_next=automatic。
使用默认值,会使用默认分配的 server_uuid:gno 分配给这个事务。
在记录 binlog 时会自动先记录一行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’;
来表示将这个事务的 GTID加入集合,从库执行时也会先检查
2、gtid_next =‘指定的值‘
通过 set gtid_next = ‘指定的值‘,然后就会检查GTID集合是否已存在这个值,如果存在下一个事务就会跳过。
例子:
库X
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; insert into t values(1,1);
记录操作在binlog相关的 GTID如下:
可以看到 insert 操作对应的 GTID 是 ‘00000000-1111-0000-1111-000000000000:2‘。此时将其设为Y的从库,在主库Y上执行 insert into t values(1,1),这条语句在实例 Y 上的 GTID 是 “aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10”;那么如何避免从库X发生异常?
答:从库(应该)执行下面的操作
set gtid_next=‘aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10‘; begin; commit; set gtid_next=automatic; start slave;
这里是执行一个空事务,作用是向从库的 GTID集合中添加主库这条记录的GTID,从库在和主库比较时就会自动跳过这条操作的事务
优点:将原本通过日志位点来匹配的方式简化了,对使用人员非常友好。
问题:如果一个新的从库接上主库,但是需要的 binlog 已经没了,要怎么做?(对应上面切换原理4.a 的情况)
答:1、如果业务允许主从不一致的情况,那么可以在主库上先执行 show global variables like ‘gtid_purged’,得到主库已经删除的 GTID 集合,假设是 gtid_purged1;然后先在从库上执行 reset master,再执行 set global gtid_purged =‘gtid_purged1’;最后执行 start slave,就会从主库现存的 binlog 开始同步。binlog 缺失的那一部分,数据在从库上就可能会有丢失,造成主从不一致。
2、如果需要主从数据一致的话,最好还是通过重新搭建从库来做。
3、如果有其他的从库保留有全量的 binlog 的话,可以把新的从库先接到这个保留了全量 binlog 的从库,追上日志以后,如果有需要,再接回主库。
4、如果 binlog 有备份的情况,可以先在从库上应用缺失的 binlog,然后再执行 start slave。
主备延迟(MySQL的可用性)
在上面的我们说到一般项目使用的都是可靠性优先策略,所以 MySQL 的可用性就决定于主从延迟。
主备延迟的来源
1、备库所在的机器性能比主库所在的机器性能差。也就是从库 binlog 落盘的速度比主库 binlog 写操作慢,主从延迟越来越高。
2、备库压力大。解决:1)使用一主多从架构 2)通过 binlog 输出到外部系统,比如 Hadoop 这类系统,让外部系统提供统计类查询的能力。
3、大事务在从库执行的时间比较长,导致数据更新的延迟变长。 解决:1)如果长事务的操作比较多,尝试将事务拆分成多个事务 2)如果是大表 DDL 语句,可以使用开源项目 gh-ost 进行调节。
4、备库的并行复制能力比较差。MySQL 的并行复制能力是比较重要的。
MySQL的并行复制
回顾一下操作执行在主备库中的流程图:
黑箭头代表主备的并行复制能力,主库上处理的是写操作,因为 InnoDB 支持行锁,再搭配数据库的多线程,吞吐量很高。而备库中读取传来解析日志的 sql_thread 是单线程,而在备库中落盘的过程也是经历了从单线程到多线程的过程。在5.6之前,备库将主库传来的日志落盘都是单线程,所以就是图中的细黑箭头。
在5.6开始,备库的落盘就变成了多线程,执行图就变成下面这种:
coordinator 就是原来的 sql_thread, 不过现在它不再直接更新数据了,只负责读取中转日志和分发事务。真正更新日志的,变成了 worker 线程。
work 线程数配置:slave_parallel_workers。这个值设置为 8~16 之间最好(32 核物理机的情况),毕竟备库还有可能要提供读查询,不能把 CPU 都吃光了。
coordinator 分发满足的基本条件:
1、更新同一行的事务必须分发给同一个worker线程
2、同一个事务的所有操作必须分给同一个worker线程
coordinator 分发规则:
1、如果跟所有 worker 都不冲突,coordinator 线程就会把这个事务分配给最空闲的 woker;
2、如果跟多于一个 worker 冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的 worker 只剩下 1 个;
3、如果只跟一个 worker 冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的 worker。
1、5.6 按库并行复制
思路:每个 worker 线程内部维护一个 hash 表,key 是 "库名",value 表示这个线程内有多少事务即将或正在执行这个库。某个事务在被分配时,会根据 coordinator 分发规则来进行。
例子:假设有一个事务T,会操作库 db1、db3,那么 coordinator 会先判断 worker_1 线程,发现存在操作 db1 的事务,所以换下一个;
紧接着发现 worker_2 也存在操作 db3 的事务,所以就会进入阻塞等待。如果后面 worker_2 中操作 db3的事务执行完毕,value变为0,那么 coordinator 就会识别到,只剩 worker_1 与事务 T 有冲突,那么就将 T 分配给 worker_1。
优点:1)在分析构造 hash 时非常快 2)对 binlog 的格式没有要求(不会发生主从不一致)
缺点:并发度不高,如果操作都集中在某一个库,那么执行就和使用单线程一样。
2、mariaDB 并行复制策略
虽然不是 MySQL 的策略,但是因为它的思想对 MySQL 后续版本有启迪作用,所以这里也说一下。
思路:因为有 " 组提交 " 机制(不知道组提交可以查看 组提交),位于一组提交的事务是可以并行复制的。一组提交的事务一定不会存在操作同一行记录的操作,这是因为主库执行写操作时,如果存在处理同一行记录的两个事务,那么其中一个一定会被行锁所阻塞," 被阻塞 " 的事务也就无法与 " 阻塞 " 事务一同提交了。执行过程就如下:
1、在一组里面一起提交的事务,有一个相同的 commit_id,下一组就是 commit_id+1;
2、commit_id 直接写到 binlog 里面;
3、传到备库应用的时候,相同 commit_id 的事务分发到多个 worker 执行;
4、这一组全部执行完成后,coordinator 再去取下一批。
缺点:1)虽然是随着主库执行顺序来执行的,但是效率并没有主库高。
主库在前一组事务提交写盘的同时下一组事务已经在执行了,而备库上需要上一组事务完全落盘后才可以开始下一组事务的执行。
主库:
备库:
2)如果一组中的一个事务是大事务,那么其他两个事务执行完还需要等待这个事务执行完才能开始下一组事务的开始
4、5.7 结合了mariaDB 的思想
MySQL 在5.7 版本融合了 mariaDB 的思想并进行了一些改进。
思想:模式分成两种,通过参数 slave-parallel-type 设置。
1)配置为 DATABASE。保持5.6 的策略,按库执行
2)配置为 LOGICAL_CLOCK。和 mariaDB 的组提交来执行,只不过进行一些变化。在 mariaDB 中是在上一组事务完全 commit 后,也就是在三步提交中这一组事务全部到达 redo log commit 阶段后才能进行下一组事务的执行;而在这个版本中则将下一组事务执行的时间提前到 redo log prepare ,只要全部到达 redo log prepare 阶段就可以执行下一组事务了。这个原因也很简单,到达 redo log prepare 阶段说明事务已经执行完成。
参数配置:binlog_group_commit_sync_delay、 binlog_group_commit_sync_no_delay_count 。这两个参数作用的都是在 binlog 完成 write 后到 fsync 的过程。分别表示 binlog 延迟多少微秒后才调 fsync、binlog 累积多少次以后才调用 fsync。通过这两个参数可以将更多的事务添加到一个组中,使备库的并发度更高。
5、5.7.22 提出了按行并行复制
在这个版本中抛弃了按库并行的思想。并且引入了按行并行。
思想:分为三种。通过参数 binlog-transaction-dependency-tracking 配置。
1)COMMIT_ORDER。还是上个版本中的按事务组来执行。
2)WRITESET(重要)。按行并行复制。对于事务涉及更新的每一行,计算这一行的 hash 值,组成集合 writeset。如果两个事务没有操作相同的行,那么他们就可以并行执行。 hash 值的计算是" 库名+表名+索引名+值 ",这里的索引指的是所有的唯一索引,值表示索引对应的值。
优势:
Ⅰ、writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容(event 里的行数据),节省了很多计算量;
Ⅱ、不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个 worker,更省内存;
Ⅲ、由于备库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也是可以的。
不足:对于“表上没主键”和“外键约束”的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型。
3)WRITESET_SESSION。在按行并行的基础上,对主库同一个线程先后事务的执行顺序,在备库执行时也需要保证。
问题:单线程添加很多记录,在从库追主库的过程中,binlog-transaction-dependency-tracking 应该选用什么参数?
答:由于主库是单线程压力模式,所以每个事务的 commit_id 都不同,那么设置为 COMMIT_ORDER 模式的话,从库也只能单线程执行。同样地,由于 WRITESET_SESSION 模式要求在备库应用日志的时候,同一个线程的日志必须与主库上执行的先后顺序相同,也会导致主库单线程压力模式下退化成单线程复制。所以,应该将 binlog-transaction-dependency-tracking 设置为 WRITESET。
主备延迟问题
原因:因为 seconds_behind_master 是备库以当前系统时间减去执行事务开始写入时间得出的。
1、主库执行了大事务(大表DDL、一个事务操作很多行)
2、备库起了一个长事务,如
begin; select * from t limit 1;
然后就不动了,这时候主库执行了一个DDL操作,添加一个字段,那么就会被堵住。
解决过期读
" 过期读 " 指的是在主库操作某行记录后,立刻进行查询,那么查询会被从库所执行,这样主库上的 binlog 还未更新到从库上,所以读请求返回的结果还是修改前的记录。下面是几种解决 " 过期读 " 的方式。
1、强制走主库(使用最多,准确)
思想:对于必须要拿到最新值的请求,强制发送给主库执行;对于可以读到旧数据的请求,发送给从库执行。
总结:实现简单,对于必须拿最新值的请求的数量不多的场景可以使用。但是在数量多的场景使用这种方式会使主库的压力变大,演变成单库模式。
2、Sleep(不准确)
思想:在从库执行前,先 sleep 一下,类似执行一条 sleep(1) 的命令。类似的业务实现比如在淘宝购买成功后并不是直接跳转到订单页面,而是跳转到 " 购买成功 " 的提示页面,如果想查看这个订单,需要点击查看订单,这就给了从库来同步主库的时间。
不足:1)因为总会延迟,如果主从延迟只有0.5秒也需要经过这个时间才能得到数据。 2)如果是大事务,应用到从库所要的时间比较长,那么还是会发生 " 过期读 "。
3、写请求在主从无延迟后再返回(不太准确)
思想
实现主备无延迟主要有四种方案。
1)查看 seconds_behind_master 是否已经等于0。seconds_behind_master 单位是秒,精度不够,所以用于检验准确性不是很高。
2)对比位点,比较 binlog 日志读取的位点。相比于第一种准确一些。通过 show slave status 查看当前从库的相关参数;通过 show master status 查看主机的相关参数。
Master_Log_File 和 Read_Master_Log_Pos,表示的是读到的主库的最新位点;
Relay_Master_Log_File 和 Exec_Master_Log_Pos,表示的是备库执行的最新位点。
如果 Master_Log_File 和 Relay_Master_Log_File、Read_Master_Log_Pos 和 Exec_Master_Log_Pos 这两组值完全相同,就表示接收到的日志已经同步完成。
3)对比当前备库和所有备库已经执行的 GTID 集合。相比于第一种准确一些。
Auto_Position=1 ,表示这对主备关系使用了 GTID 协议。
Retrieved_Gtid_Set,是备库收到的所有日志的 GTID 集合;
Executed_Gtid_Set,是备库所有已经执行完成的 GTID 集合。
如果这两个集合相同,也表示备库接收到的日志都已经同步完成。
问题:2)、3)为什么会不准确?
答:一个事务的执行顺序如下:
Ⅰ、主库执行完成,写入 binlog,并反馈给客户端;
Ⅱ、binlog 被从主库发送给备库,备库收到;
Ⅲ、在备库执行 binlog 完成。
如果在1、2之间判断就会以为是最新的,没有问题,直接进行操作,导致 " 过期读 "。
4)使用 semi-sync(搭配2、3)。semi-sync 是半同步复制(emi-sync replication),而2、3 是异步复制,也就是在主库写完 binlog 后就会发送消息给客户端。而执行过程如下:
Ⅰ、事务提交的时候,主库把 binlog 发给从库;
Ⅱ、从库收到 binlog 以后,发回给主库一个 ack,表示收到了;
Ⅲ、主库收到这个 ack 以后,才能给客户端返回“事务完成”的确认。
通过 semi-sync 可以解决2、3出现的问题,同时在主库异常断电时也可以避免主库在写完 binlog 还未将 binlog 发送给从库就宕机,导致从库未收到 binlog 导致主库重启后主从不一致。
缺点
1)只适用于一主一从。因为这个模式的执行过程是主库收到 ACK 确认后立刻就会反馈给请求发送方信息,如果是一主多从,那么任何一台从机返回的 ACK 都会使主库立刻返回反馈信息给请求,这样如果再次发送读该数据的请求,并且请求被分配到还未同步的从库,那么又会发生 "过期读"。
2)在持续延迟的情况下,可能出现过度等待的问题。
如果按2,3判断,直到状态4都无法执行读操作,而实际上在状态2就可以读了。
4、先在一段时间内尝试在从库上执行,如果没有同步再在主库上执行(准确)
这种方式有两种实现方案。
1)等主库位点方案
原理:
实现主要通过命令:select master_pos_wait(file, pos, timeout); 特点如下:
Ⅰ、它是在从库执行的;
Ⅱ、参数 file 和 pos 指的是主库上的文件名和位置;
Ⅲ、timeout 可选,设置为正整数 N 表示这个函数最多等待 N 秒。
Ⅳ:返回值:一、正常返回的结果是一个正整数 M,表示从命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务。
二、如果执行期间,备库同步线程发生异常,则返回 NULL;
三、如果等待超过 N 秒,就返回 -1;
四、如果刚开始执行的时候,就发现已经执行过这个位置了,则返回 0。
执行逻辑:
假设客户端接收的延迟为1秒,那么执行逻辑就如下:
Ⅰ、trx1 事务更新完成后,马上执行 show master status 得到当前主库执行到的 File 和 Position;
Ⅱ、选定一个从库执行查询语句;
Ⅲ、在从库上执行 select master_pos_wait(File, Position, 1);
Ⅳ、如果返回值是 >=0 的正整数,则在这个从库执行查询语句;
Ⅴ、否则,到主库执行查询语句。
2)GTID 方案
原理:
主要通过命令: select wait_for_executed_gtid_set(gtid_set, timeout);
语句作用:等待,直到这个库执行的事务中包含传入的 gtid_set,返回0;超时返回1。
执行逻辑:
还是假设客户端接收的延迟为1秒,执行逻辑如下:
Ⅰ、trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid1;
Ⅱ、选定一个从库执行查询语句;
Ⅲ、在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);
Ⅳ、如果返回值是 0,则在这个从库执行查询语句;
Ⅴ、否则,到主库执行查询语句。
在上面的第Ⅰ步中,trx1 事务更新完成后,从返回包直接获取这个事务的 GTID。我们只需要将参数 session_track_gtids 设置为 OWN_GTID,然后通过 API 接口 mysql_session_track_get_first 从返回包解析出 GTID 的值即可。
问题:假设你的系统采用了我们文中介绍的最后一个方案,也就是等 GTID 的方案,现在你要对主库的一张大表做 DDL,可能会出现什么情况呢?为了避免这种情况,你会怎么做呢?
答:假设,这条语句在主库上要执行 10 分钟,提交后传到备库就要 10 分钟(典型的大事务)。那么,在主库 DDL 之后再提交的事务的 GTID,去备库查的时候,就会等 10 分钟才出现。这样,这个读写分离机制在这 10 分钟之内都会超时,然后走主库。
这种预期内的操作,应该在业务低峰期的时候,确保主库能够支持所有业务查询,然后把读请求都切到主库,再在主库上做 DDL。等备库延迟追上以后,再把读请求切回备库。
这个思考题主要是想关注大事务对等位点方案的影响。当然了,使用 gh-ost 方案来解决这个问题也是不错的选择。
优点:相比于使用位点的方案,减少了一次在主库上的查询操作。
缺点:使用前提是开启了 GTID,使用场景没有使用位点广泛。