TOC
本文为 「极客时间- MySQL 实战 45 讲」 学习笔记;主要目的是加深理解;
概要
下图是经典的 MySQL 读写分离架构,其中 A 机为主库,A’ 机为 A 机的备库;B, C, D 均为只读从库;引用自 「极客时间- MySQL 实战 45 讲」;
读写分离架构中,主库允许读写,而从库只允许读。主库进行写入后,MySQL 会通过主从机制将数据同步给从库。当然主从同步是需要一定时间的,如果客户端请求写入主库后,客户端立刻查询从库,则会读取到从库还未同步主库的结果,这种现象我们称为「过期读」;
解决过期读,我们有下列几种方法:
- 强制走主库方案
- 强制等待方案
- 检测主从无延迟方案
- 半同步方案
- 等待主库位点方案
- 等待 GTID 方案
强制走主库方案
强制走主库方案,就是将查询需求分类;可将查询需求分为以下两类:
- 查询请求必须拿到最新结果,则将请求强制发送到主库上;
- 查询请求允许读到旧数据,则可以将请求发送到从库上;
这种方式,在实际运用中用的很多,但是对于金融业务场景,很可能所有查询请求都必须拿到最新结果,此时读写分离的水平扩展能力完全失效;
下面看看支持读写分离的解决「过期读」的方案
强制等待方案
强制等待方案,就是主库进行写入后,从库的读请求,强制等待一定时间再进行处理读操作;类似与从库的查询请求,先执行类似 select sleep(1) 的命令;
我们可以看出这是一个不靠谱的方案,每次查询都需要先 sleep 再查询,用户体验十分不好,并且等待时间也无法精确;下面为了让方案更靠谱,一般业务场景设置为如下:
用户进行更新操作后,希望立刻查看更新是否成功;此时系统可以直接将更新信息放到查询页面中,不进行数据库查询;当用户对页面进行刷新时,再从数据库查询;
尽管经过业务改造,方案还是不靠谱的;主要查询等待时间不精确,不精确体现在两个方面;
- 从库只需要 0.5s 同步完成,查询请求还是需要等待指定时间 1s;
- 从库可能 1s 还未同步完成,查询请求任然是过期读;
下面展示更加精确的方案
检测主从无延迟方案
检测主从无延迟方案,主要思路是执行查询请求前,先判断主从延迟情况;具体执行步骤如下:
- 先判断主从延迟情况;
- 若无延迟,则直接在从库中执行查询;
- 若有延迟,则等待一定时间;
- 若指定等待时间内, 主从同步完成,则直接在从库中执行查询;
- 若指定等待时间为同步完成,则抛弃请求或直接在主库执行查询请求;
步骤 2,3,4,5 都很好理解实现;而如何判断主从延迟情况呢? MySQL 提供了三种方式
- 对比 seconds_behind_master 确保主备无延迟
- 对比同步位点确保主备无延迟
- 对比 GTID 集合确保主备无延迟
对比 seconds_behind_master 确保主备无延迟
在从库中执行 show slave status,获取参数 seconds_behind_master, 参数值表示的是从库相对于主库延迟多少秒;
当在从库查询时,可以判断 seconds_behind_master 是否为 0 来确认主从是否有延迟;但是这种方案只能精确到秒;
对比同步位点确保主备无延迟
在从库中执行 show slave 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 这两组值完全相同,就表示接收到的日志已经同步完成。
对比 GTID 集合确保主备无延迟
在从库中执行 show slave status 获取以下三个参数
- Auto_Position = 1,表示主备关系使用了 GTID 协议。
- Retrieved_Gtid_Set,是备库收到的所有日志的 GTID 集合;
- Executed_Gtid_Set,是备库所有已经执行完成的 GTID 集合。
如果集合 Retrieved_Gtid_Set 和 Executed_Gtid_Set 相同,表示备库接收到的日志都已经同步完成。
从上述三个方案中,可以看出,对比同步位点和对比 GTID 的方式要比判断 seconds_behind_master 要更加精确,这三种方案相比「强制等待方案」要准确不少;但还未达到精确的程度,仍然会出现「过期读」的现象;
MySQL 一个事务的主从同步流程大概如下:
- 主库执行完成,写入 binlog, 并返回结果至客户端;
- 主库将 binlog 发送给从库,从库收到;
- 从库执行 binlong, 进行同步,同步完成;
当客户端执行完更新操作,并且在步骤二主库发送 binlog 给从库前,执行查询操作;此时从库由于没有收到 binlog ,会认为主从无延迟;此时查询会直接在从库执行,也就出现了「过期读」;
「检测主从无延迟方案」还有一个缺点,那就是更新高峰期,主库的位点或者 GTID 集合更新很快,那么上面的两个位点等值判断就会一直不成立,很可能出现从库上迟迟无法响应查询请求的情况。
半同步方案
要解决「检测主从无延迟方案」的过期读问题,就要引入半同步复制「semi-sync replication」;
在半同步方案中,一个事务的主从同步流程大概如下:
- 主库执行完成,写入 binlog;
- 从库收到 binlog 以后,发回给主库一个 ack,表示收到了;
- 主库收到这个 ack 以后,才能给客户端返回“事务完成”的确认;
因此,启动了半同步机制,主库在返回给客户端前,确保了从库获取到了 binlog;此时再配置同步位点或 GTID 集合方案,则不再出现「过期读」现象;但是这也只适用于一主一从的情况;
因为,主库确保了一个从库收到 binlog,就返回结果给客户端了;
到目前为止,使用半同步 + 检测主从延迟,还有以下两个问题:
- 由于 binlog 发送机制,部分从库,可能出现「过期读」现象;
- 持续延迟的情况下,可能出现过度等待现象;
等待主库位点方案
首先,介绍一条命令
select master_pos_wait(file, pos[, timeout]);
这条命令的逻辑如下:
- 在从库执行的;
- 参数 file 和 pos 指的是主库上的文件名和位置;
- timeout 可选,设置为正整数 N 表示这个函数最多等待 N 秒。
这个命令正常返回的结果是一个整数 M;
- 如果正常执行,返回一个正整数,表示命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务;
- 如果执行过程,出现主从同步异常,返回 NULL;
- 如果等待时间超过 N 秒,返回 -1;
- 如果刚开始执行,发现已经同步了这个位置,返回 0;
下面介绍「等待主库位点方案」流程:
- 事务 trx1 更新完成后,马上执行 show master status 得到当前主库执行到的 File 和 Position;
- 选定一个从库执行查询语句;
- 在从库上执行 select master_pos_wait(File, Position, 1);
- 如果返回值是 >=0 的正整数,则在这个从库执行查询语句;
- 否则,到主库执行查询语句或超时放弃。
等待 GTID 方案
如果你的数据库开启了 GTID 模式,对应的也有等待 GTID 的方案,MySQL 提供了一条相似的命令;
select wait_for_executed_gtid_set(gtid_set, 1);
这条命令的逻辑是:
- 等待,直到这个库执行的事务中包含传入的 gtid_set,返回 0;
- 超时返回 1。
等待 GTID 方案,通过设置 session_track_gtids 为 OWN_GTID;可在事务 trx1 更新完成返回包中,直接解析出 GTID;因此,相对于「等待主库位点方案」,少一条 show master status 命令;
总结
本文主要解决「过期读」的问题,首先,我们能想到的最简单的方案就是将业务请求分类,对于允许「过期读」的分流至从库,否则只允许读主库;
然后为了解决大部分业务都不允许「过期读」的问题,我们引入了一个「不靠谱方案」– 延时读。通过业务改造或强制请求等待,使得从库有足够的时间同步主库内容;
接着,我们引入了一个较为靠谱的方案,检测同步情况,根据同步情况进行分流主从库;检测同步本文讲解了以下三种方案
- 对比 seconds_behind_master (只能精确到秒级)
- 对比同步位点
- 对比 GTID 集合
由于 MySQL 主从同步机制,事务请求结束后,会立刻返回,然后再发送 binlog 至从库;所以仍旧有可能出现「过期读」,我们引入了半同步方案,主库确保一个从库收到 binlog 后再返回给客户端;解决了一主一备的情况的「过期读」;
最后,我们介绍了下面两个命令,才完全解决「过期读」的问题;
select master_pos_wait(file, pos[, timeout]);
select wait_for_executed_gtid_set(gtid_set, 1);
实际开发过程中,我们是会多个方案组合,根据业务权衡选择方案,并非一定采用完全解决「过期读」的翻案;其中业务请求分类,以及「不靠谱方案」– 业务改造,延迟请求,日常开发过程中使用会很广泛;