正常的一主多从的状态如下:主库负责主要写请求和部分读,从分担大部分的读请求。但是因为主从可能出现延迟,如果客户端执行完一个事务之后马上发起查询,就可能会出现 “过期读” 的现象,也就是读到一个不是最新的更新数据。
我们有如下方案来解决这个问题:
- 强制走主库方案;
- sleep 方案;
- 判断主备无延迟方案;
- 配合 semi-sync 方案;
- 等主库位点方案;
- 等 GTID 方案。
28.1强制走主库
强制走主库的方案其实就是将请求做分类,大致将请求分为两类:
- 对于需要最新数据的请求就走主库
- 对于可以使用旧数据应用的走从库
28.2sleep方案
主库更新完后,从库查询前先sleep一下。也就是在每条从从库过的请求,我们都给它sleep(n)n秒,用来掩盖同步延迟的时间。
28.3判断主备无延迟方案
1.对比同步延迟时间
通过show slave status
结果下的seconds_behind_master参数的值来得到主备延迟时间的长短。
每次请求前,我们需要去请求主备延迟时间,只有等于0的情况下,才可以去执行请求。
2.对比日志位点
- 通过参数Master_Log_File 和 Read_Master_Log_Pos,表示的是读到的主库的最新位点;
- 通过参数Relay_Master_Log_File 和 Exec_Master_Log_Pos,表示的是备库执行的最新位点。
要这两组数据完全相同时,才可以执行请求。
3.对比GTID集合
通过show slave status
指令来 Retrieved_Gtid_Set 和 Executed_Gtid_Set 参数,这两个参数分别表示着 备库收到的所有日志的 GTID 集合, 备库所有已经执行完成的 GTID 集合。
通过对比集合是否相同,看日志是否已经同步完成。
这里有个问题,一个事务的 binlog 在主备库间的流程:
- 主库执行完成,写入 binlog,并反馈给客户端;
- binlog 被从主库发送给备库,备库收到;
- 在备库执行 binlog 完成。
这样子就可以出现,客户端已经收到提交确认,但是备库却没有收到binlog的情况。解决问题如下:
28.4 配合 semi-sync
semi-sync 做了这样的设计:
- 事务提交的时候,主库把 binlog 发给从库;
- 从库收到 binlog 以后,发回给主库一个 ack,表示收到了;
- 主库收到这个 ack 以后,才能给客户端返回“事务完成”的确认。
当时semi-sync有如下问题,①如果备库过渡等待,也导致事务也过渡等待;②在一主多从情况下,客户端在收到任意一个ack后,便确认事务已经完成,但这还是会出现过期读的现象,读取的从库不是提交ack的那个库。
下面介绍两种方案解决这个问题:
28.5 等主库位点方案
看,新命令如下:
select master_pos_wait(file, pos[, timeout]);
这条命令的逻辑如下:
- 它是在从库执行的;
- 参数 file 和 pos 指的是主库上的文件名和位置;
- timeout 可选,设置为正整数 N 表示这个函数最多等待 N 秒。
这个命令正常返回的结果是一个正整数 M,表示从命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务。
当然,除了正常返回一个正整数 M 外,这条命令还会返回一些其他结果,包括:
- 如果执行期间,备库同步线程发生异常,则返回 NULL;
- 如果等待超过 N 秒,就返回 -1;
- 如果刚开始执行的时候,就发现已经执行过这个位置了,则返回 0。
使用方式:
- trx1 事务更新完成后,马上执行 show master status 得到当前主库执行到的 File 和 Position;
- 选定一个从库执行查询语句;
- 在从库上执行 select master_pos_wait(File, Position, 1);
- 如果返回值是 >=0 的正整数,则在这个从库执行查询语句;否则,到主库执行查询语句。
28.6 GTID 方案
如果你的数据库开启了 GTID 模式,对应的也有等待 GTID 的方案。MySQL 中同样提供了一个类似的命令:
select wait_for_executed_gtid_set(gtid_set, 1);
流程图如下:
- trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid1;
- 选定一个从库执行查询语句;
- 在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);
- 如果返回值是 0,则在这个从库执行查询语句;
- 否则,到主库执行查询语句。
将参数 session_track_gtids 设置为 OWN_GTID ,就可以让事务的返回包带有GTID。