在读写分离的情况下,也就是主库主要负责写数据,读取数据得压力都分布在从库上。由于主从延迟是无法避免得,所以如果在主库中执行完一个事务之后,立即发起一个查询,这个时候在从库上查询得到的数据可能是刚刚事务之前的状态。这种在从库上读取到的一种过期的状态,暂时称为过期读。处理过期读的方案有如下几个:
- 强制走主库
- sleep方案
- 判断主备有无延迟方案
- 配合semi-sync方案
- 等主库位点方案
- 等主库GITD方案
强制走主库
就是将查询请求分为两类,一类是业务上需要实时查询的,另一类是不需要的。业务上需要实时查询的就让走主库查询。但是如果业务都需要走实时查询的话,那读写的压力都在主库上了,就等于放弃了读写分离,放弃了扩展性。
sleep方案
主库更新之后,每次去到从库的查询请求都先sleep一下,具体的方案就是类似于执行一条sleep sleep(1)命令。也就是查询请求之前,先等待一秒。但是这样会有两个问题:
- 如果从库执行relay log的时间超过了一秒,那么还是会出现过期读的情况。
- 每次查询请求都sleep一秒,如果大部分执行 relay log的时间都不超过1秒,那就等于浪费了时间,影响了用户体验。
判断主备无延迟方案
之前中的showslave status命令中结果里的seconds_behind_master参数的值,可以用来衡量主备延迟时间的长短。每次从库执行查询请求之前,先判断seconds_behind_master这个参数的值是不是为0,如果不等于0,就一直等待到等于0的时候,seconds_behind_master的精度是秒。除了判断这个参数的值之外,还有另外两种方式来判断主从延迟。
第一种就是判断同步位点的方式:
- Master_Log_File和Read_Master_Log_Pos,表示的是当前读取到的主库中的最新位点。
- Relay_Master_Log_File和Exec_Master_Log_Pos,表示的是从库中执行的最新位点。
如果两组位点位置是相同的话,表示数据已经同步完成。
第三种方式就是GTID集合对比的方式
- Auto_Position=1 表示对主备关系开启了GTID协议。
- Retrieved_Gtid_Set,是备库中收到的所有日志的GTID集合。
- Executed_Gtid_Set, 是备库中所有已经完成的日志的GTID集合。
如果两个集合相同,那么也表示数据同步完成。这两种方式都要比seconds_behind_master参数值=0来的更准确。
这几种判断主备无延迟的判断标准都是备库将接收到的relay log都已经执行完毕了。但是存在这样一个问题:
- 客户端发起一个事务请求
- 主库提交事务完成,写入binglog,日志发送给备库,与此同时客户端收到主库的确认结果。
- 备库还没有收到binlog日志,客户端已经开始查询,因为这个时候备库收到的所有relay log日志都是已经执行过的,所以认为无延迟。
- 但是客户端无法在备库中查询到最新数据,还是出现了过期读的状态。
配置semi-sync
为了解决上面出现的问题,就要引入半同步复制,也就是semi-sync replication.
- 事务提交的时候,主库把binlog发送给备库
- 备库收到binlog日志,回给主库一个ack确认标记,表示收到了
- 主库在得到备库返回的ack标记之后,才想客户端返回确认结果。
但是semi-sync配置前面同步位点的判断,只会对一主一备的情况下是成立的,因为主库在讲binlog日志发送给多个从库的情况下,只要有一个从库返回了ack标志,那么主库就会给客户端返回确认结果。
所以如果查询的请求是在返回ack标志的从库中执行的,那可以确保是可以读取到最新数据的;但是如果查询的请求不是在返回ack标志的从库的,那么就会有过期读的情况存在。
并且判断同步位点还是有另外一个问题的,客户端发起了一个请求A,随后又有很多请求也一起到了主库中,主库执行完事务A和之后所有的事务请求之后,将binlog日志发送给备库。虽然从库中已经将最先进入的事务A提交了,并且数据也已经更新到了最新,但是由于从库中存在还未同步完成的relay log日志,所以认为此时是存在主备延迟的(同步位点判断的标准就是从库中所有收到的relay log日志已经执行完成)。这个时候其实是可以避免的,因为最新进入的事务A已经完成了,那么查询事务A涉及到的数据可以认为是没有延迟的。
等主库位点方案
先介绍一条命令:
select master_pos_wait(file, pos[, timeout]);
这是在从库中执行的,file和pos指的是主库中的问题名,timeout是一个可选指定超时参数。
正常情况下这个命令会返回一个正整数,但是也会在特定情况下返回其他数字:
- 如果执行期间,同步日志的线程发生了异常,会返回null
- 如果等待时间超过timeout指定的时间,就会返回-1
- 如果执行的时候发现之前执行过这个位置了,就返回0(这个每太懂,是发现查询过这个位置了,就直接返回0么?还是说防止两次同样的查询?)
那么在主库中执行完一个更新事务请求之后,马上执行show master status得到当前最新的file和position;然后在从库的查询请求中执行
select master_pos_wait(file,positon,timeout)命令,前两个参数就是主库中得到的file和position;如果返回结果是一个正整数,那就说明查询数据的日志已经同步完成过了,就直接查询从库,否则的话就直接查询主库。
等GITD方案
其实和上面的类似,也有一条查询命令:
select wait_for_executed_gtid_set(gtid_set, 1);
是在从库中执行的,第一个参数是指定的gtid,也就是一个事务提交完成之后分配的gtid,返回的逻辑是:
- 等待,直到这个库中已经执行过的gtid_set集合中包含这个指定的gtid,返回0
- 超时返回1