很多业务场景一般是读多写少,但是MySQL的单机处理能力一般有限,为此一般通过水平扩展出多个从机用于提供读服务,以分担写库的压力。但是读写分离后,写库执行完事务后,同步到备机,备机执行完同样的事务,往往有一个时间差,这个时间差叫主备延迟。从而导致客户端去从机读取,读取不到最新的数据,这就是过期读问题。

主备延迟监控

主备延迟可以通过在备机执行命令

SHOW SLAVE STATUS;

这个命令的输出中有一个seconds_behind_master字段,这个字段的值就是主备延迟的时间,以秒为单位。
它是这样计算出来的:
1、主库在提交事务的时候,会往binlog中写入当前的时间;
2、当备库执行当前事务的时候,拿到主库的写入的时间与系统的当前时间对比,两者的差值就是主备延迟的时间。

主备延迟的产生原因

由于主备延迟,会导致过期读的问题,为此我们需要尽量减小主备延迟,那么知道主备延迟是怎么产生的就非常有必要了。
1、备机的硬件性能比较差。很多人往往会认为,备机的压力相对主机来说会小很多,为此配置比较差的硬件。但是IOPS对于主备是无差别的,而且主备随时可能都会发生主备切换,为此主备的机器硬件配置往往是一致的。
2、备机压力过大。备机往往会提供读服务,但是读服务的QPS可能会非常高,如果全部都打到一台备机上,那么单台备机的压力往往会很大。为此我们一般通过如下的方式解决:
A.配置多台备机,以水平的扩展读请求,避免单台备机压力过大;
B.引入外卖存储,如redis缓存,hadoop等。
3、大事务。主要包括:
A.在一个事务里面增删改大量的行数据;
B.大表的DDL;
C.小表的DDL,但是在备机上刚好执行了一个查询的大事务,将DDL堵塞。
4、备机复制能力。在5.6版本之前的MySQL只支持单线程复制,如果你还是用的5.6之前的版本,那么需要将版本升级到5.6及5.6以后的支持并行复制的版本。

Mysql 5.6并行复制

按库并行。每一个worker上构建一个hash列表,同一个库的事务会被分配一个worker里。
优势:
A.以库为单位,构建hash列表会很快且不会太大
B.binlog的格式不需要是row,statement格式的日志也可以获取到库名
缺陷:
粒度太大,库内无法并行,对于那些业务大部分集中在某个db的场景,效率接近于单线程复制

MariaDB并行复制

按组提交(利用redo log的组提交特性,在同一组提交的事务一定不会修改同一行)。在一组里一起提交的事务,在binlog里都会有一个相同的commit_id,传到备库后相同commit_id的事务可以分发到多个worker执行
缺陷:
不在同一组的事务无法在备库上并行,备库的吞吐量不足,且容易被大事务影响

Mysql 5.7并行复制

增加参数slave-parallel-type来控制并行复制策略:
A.配置为database,则按照Mysql5.6的策略执行
B.配置为logical_clock,则按照一种类似于MriaDB的思路进行:
1)同时处于 prepare 状态的事务,在备库执行时是可以并行的
2)处于 prepare 状态的事务,与处于 commit 状态的事务之间,在备库执行时也是可以并行的。
可以通过参数binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count来拉长binlog write和fsync之前的时间,制造更多"同时处于"prepare阶段的事务

Mysql 5.7.22并行复制

在原有的基础上新增了writeset的并行复制策略,通过参数binlog-transaction-dependency-tracking来控制,可选值有三种:
A.COMMIT_ORDER:之前5.7版本的并行复制方式
B.WRITESET:对事务涉及更新的每一行计算hash,组成集合WRITESET,两个事务的WRITESET没有交集的话,那么他们可以并行执行(表上无主键or表上有外键时,使用该策略无法并行)
C.WRITESET_SESSION:在WRITESET的基础上增加约束,即在主库上同一线程先后执行的两个事务,在备库上也要保证其执行的先后顺序

WRITESET的优势:
1)WRITESET是在主库执行后写到binlog里的,因此备库执行不需要解析binlog来生成WRITESET
2)分发任务时不需要扫描事务的整个binlog内容
3)由于备库分发策略不依赖于binlog的内容,所以binlog的格式可以是statement的

过期读解决方案

使用读写分离方案后,由于主从存在延迟,当客户端发起读库时,如果请求路由到从库,那么可能读取不到最新的数据,这个就叫做过期读。那么过期读都有什么解决方案呢?

强制走主库方案

我们可以把请求进行分类,对于允许出现过期读的场景,走从库;对于不允许过期读的场景强制走主库的方案。
这个方案实施起来最简单,也使用的最多。但是这个方案可能会有问题,再某些领域,可能大部分甚至所有的场景都不允许过期读,为此我们还需要配合其他方案。

sleep方案

sleep方案指的是在查询从库之前等待一段时间,然后再查询,等到真正查询的时候,日志可能已经同步到从库了。你可能会说这个方案看起来会严重影响客户的体验。其他我们可以换一种思路实现,比如博客平台上面发布博客,当客户点击发布博客的时候,我们可以直接显示客户的博客内容到页面上,当客户再次刷新博客的时候,在从从库读取,此时大概率可以读取到博客了。
但是这个方案也有问题,有时主从同步延迟可能会特别长,那么客户再次访问的时候,可能还是读取不到最新的数据。

主从无延迟方案

主从无延迟方案,指的是在读从库之前,先判断主从是否有延迟,如果没有延迟,那么从从库读取,如果有延迟则等待一定的时间,若等待的时间内主从还是有延迟,可以返回报错或者去主库读取。但是如果超时去主库读取,可能会导致大量的请求打到主库上,为此主库需要做好限流。
那么主从无延迟有什么判断标准呢?我们可以通过命令

SHOW SLAVE STATUS;

这个命令的输出中包括了如下的字段

Seconds_Behind_Master //表示主从延迟的秒数

Master_Log_File
Read_Master_Log_Pos//这两个指标指的是最新的主库的位点信息
Relax_Master_Log_File
Exec_Master_Log_Pos//这两个指标指的是已执行的主库的位点信息

Retrieved_Gtid_Set//这个指标指的是最新的主库的GTID集合
Executed_Gtid_Set//这个指标指的是已执行的主库的GTID集合
Auto_Position//表示主从同步使用了GTID方案

他们分别对于了如下的三种方式
1、通过Seconds_Behind_Master判断:若Seconds_Behind_Master=0,,则表示主从无延迟。但是这个指标是以秒为单位的,不够精确,可能存在relax_log还没有同步。
2、通过位点判断:如果Master_Log_File与Relax_Master_Log_File相等 并且 Read_Master_Log_Pos与Exec_Master_Log_Pos相等,则表示主从无延迟。
3、通过GTID集合判断:Auto_Position=1 且 Retrieved_Gtid_Set与Executed_Gtid_Set相等,则表示主从无延迟。
但是他们三个都有一个共同的缺点:他们都是通过对比从机接收到的最新的日志与已经执行的日志,得到值。那么主库已经提交的日志,备库可能还没有收到。那么当判断主从无延迟的时候,实际上未必真的无延迟,这样还是存在过期读的问题。

semi-sync半同步方案

由于主从无延迟方案,还是存在过期读的情况。为此我们引入semi-sync半同步方案。它是通过主从无延迟方案配合semi-sync以解决过期读问题。
所谓的semi-sync,指的是单主机提交事务的时候,需要保证将日志至少同步给了其中一个从机(也就是至少有一个从机收到了主库的binglog),然后在回复客户端。注意这里是“同步给了其中一个从机”,也就是说,只要同步给了其他一台主机,则可以返回客户端了。为此如果需要彻底解决过期读的问题,只能有一个从库。但是实际的生产环境中往往有多台从机。

等位点方案

MySQL提供了如下的语句

SELECT wait_master_pos(file, pos [,timeout])

这个函数会返回如下的值:
1、>=0:从这个函数开始执行到日志执行到指定位点时,执行的事务数。
2、-1:等待执行到指定位点超时。
3、NULL:主从同步出错。
为此如果函数返回>=0,则指定的位点的事务在从库已经提交了。如果我们再主库执行完事务后,能够获取到最新的位点信息,返回给客户端,那么客户端查询从库的时候可以先通过函数wait_master_pos判断位点是否已经同步,如果已经同步则从从库读取数据,否则从主库读取或者返回超时错误。如果从主库读取,同样的需要做好限流。
我们在主库执行完SQL语句的时候,立即执行如下的语句:

SHOW MASTER STATUS;

以得到最新的位点信息,返回给客户端。

等GTID方案

如果你的主从同步开启了GTID方案,那么相应的也有等待GTID方案。相似的MySQL提供了如下的语句

SELECT wait_for_executed_gtid_set(gtid_set [,timeout])

监视应用于服务器上的所有gtid,包括从所有复制通道和用户客户机到达的事务。
如果指定了超时,并且在GTID集中的所有事务应用之前超时时间已经过去,则函数停止等待。超时是可选的,默认超时为0秒,在这种情况下,函数总是等待,直到应用了GTID集中的所有事务。它会返回如下的值:
0:表示在超时的时间内,指定的GTID集合已经执行了。
1:表示指定的GTID集合执行超时。
为此如果返回0,则指定GTID集合的事务在从库已经提交了。相似的我们也可以通过

SHOW MASTER STATUS;

在“Executed_Gtid_Set”列中,获取到主库提交事务后的GTID集合。不过从MySQL5.7.6开始,可以通解析API的返回值,得到GTID集合,这样就少了一个服务往返时间:

int mysql_session_track_get_first(MYSQL *mysql, enum enum_session_state_type type, const char **data, size_t *length)

其官方文档见:mysql-session-track-get-first。从官方文档可以看出,需要打开参数:

session_track_gtids