Open lihongjie0209 opened 3 years ago
主库更新后,读从库之前先sleep一下。具体的方案就是,类似于执行一条select sleep(1)命令。
这个方案的假设是,大多数情况下主备延迟在1秒之内,做一个sleep可以有很大概率拿到最新的数据。
这个方案给你的第一感觉,很可能是不靠谱儿,应该不会有人用吧?并且,你还可能会说,直接在发起查询时先执行一条sleep语句,用户体验很不友好啊。
但,这个思路确实可以在一定程度上解决问题。为了看起来更靠谱儿,我们可以换一种方式。
以卖家发布商品为例,商品发布后,用Ajax(Asynchronous JavaScript + XML,异步JavaScript和XML)直接把客户端输入的内容作为“新的商品”显示在页面上,而不是真正地去数据库做查询。
这样,卖家就可以通过这个显示,来确认产品已经发布成功了。等到卖家再刷新页面,去查看商品的时候,其实已经过了一段时间,也就达到了sleep的目的,进而也就解决了过期读的问题。
也就是说,这个sleep方案确实解决了类似场景下的过期读问题。但,从严格意义上来说,这个方案存在的问题就是不精确。这个不精确包含了两层意思:
所以第一种确保主备无延迟的方法是,每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0。如果还不等于0 ,那就必须等到这个参数变为0才能执行查询请求。
但是
Seconds_Behind_Master: The number of seconds that the replication SQL thread is behind processing the source's binary log. A high number (or an increasing one) can indicate that the replica is unable to handle events from the source in a timely fashion.
这个参数只是表示IO线程和SQL线程的延时, 如果IO线程没有获取到最新的binlog, 那么这个参数是无效的,
第二种方法,对比位点确保主备无延迟:
如果Master_Log_File和Relay_Master_Log_File、Read_Master_Log_Pos和Exec_Master_Log_Pos这两组值完全相同,就表示接收到的日志已经同步完成。
第三种方法,对比GTID集合确保主备无延迟:
如果这两个集合相同,也表示备库接收到的日志都已经同步完成。
可见,对比位点和对比GTID这两种方法,都要比判断seconds_behind_master是否为0更准确。
要理解等主库位点方案,我需要先和你介绍一条命令:
select master_pos_wait(file, pos[, timeout]);
这条命令的逻辑如下:
这个命令正常返回的结果是一个正整数M,表示从命令开始执行,到应用完file和pos表示的binlog位置,执行了多少事务。
当然,除了正常返回一个正整数M外,这条命令还会返回一些其他结果,包括:
对于图5中先执行trx1,再执行一个查询请求的逻辑,要保证能够查到正确的数据,我们可以使用这个逻辑:
我把上面这个流程画出来。
图6 master_pos_wait方案
这里我们假设,这条select查询最多在从库上等待1秒。那么,如果1秒内master_pos_wait返回一个大于等于0的整数,就确保了从库上执行的这个查询结果一定包含了trx1的数据。
步骤5到主库执行查询语句,是这类方案常用的退化机制。因为从库的延迟时间不可控,不能无限等待,所以如果等待超时,就应该放弃,然后到主库去查。
你可能会说,如果所有的从库都延迟超过1秒了,那查询压力不就都跑到主库上了吗?确实是这样。
但是,按照我们设定不允许过期读的要求,就只有两种选择,一种是超时放弃,一种是转到主库查询。具体怎么选择,就需要业务开发同学做好限流策略了。
如果你的数据库开启了GTID模式,对应的也有等待GTID的方案。
MySQL中同样提供了一个类似的命令:
select wait_for_executed_gtid_set(gtid_set, 1);
这条命令的逻辑是:
在前面等位点的方案中,我们执行完事务后,还要主动去主库执行show master status。而MySQL 5.7.6版本开始,允许在执行完更新类事务后,把这个事务的GTID返回给客户端,这样等GTID的方案就可以减少一次查询。
这时,等GTID的执行流程就变成了:
跟等主库位点的方案一样,等待超时后是否直接到主库查询,需要业务开发同学来做限流考虑。
我把这个流程图画出来。
图7 wait_for_executed_gtid_set方案
在上面的第一步中,trx1事务更新完成后,从返回包直接获取这个事务的GTID。问题是,怎么能够让MySQL在执行事务后,返回包中带上GTID呢?
你只需要将参数session_track_gtids设置为OWN_GTID,然后通过API接口mysql_session_track_get_first从返回包解析出GTID的值即可。
强制走主库
强制走主库方案其实就是,将查询请求做分类。通常情况下,我们可以将查询请求分为这么两类:
你可能会说,这个方案是不是有点畏难和取巧的意思,但其实这个方案是用得最多的。
当然,这个方案最大的问题在于,有时候你会碰到“所有查询都不能是过期读”的需求,比如一些金融类的业务。这样的话,你就要放弃读写分离,所有读写压力都在主库,等同于放弃了扩展性。