|
读写分离的基本结构:
上图的结构是客户端主动做负载均衡,这种模式下一般会把数据库连接信息放在客户端的连接层,由客户端选择后端数据库进行查询。
还有一种架构是在MySQL和客户端间加入中间代理层proxy,客户端只连接proxy,由proxy根据请求类型和上下文决定请求的分发路线:
比较这两种架构:
但不论使用哪种架构,由于主从延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库,都有可能读到刚刚的事务更新之前的状态。
暂时称这种“在从库上读到系统的一个过期状态”的现象为过期读。客户端肯定希望查询从库的数据结果和查主库的数据结果相同,因此接下来就讨论如何处理过期读问题,本文涉及到的处理方案有:
-
强制走主库方案;
-
sleep方案;
-
判断主备无延迟方案;
-
配合semi-sync方案;
-
等主库位点方案;
-
等GTID方案。
强制走主库方案
强制走主库其实就是将查询请求做分类,通常查询请求分为两类:
该方案的最大问题是有时候碰到所有查询都必须拿到最新结果的需求,所有读写压力实际上都在主库,即放弃了读写分离。
Sleep方案
主库更新后,读从库之前先sleep,比如执行一条select sleep(1)命令。该方案就是假设大多数情况下主备延迟在1秒内,sleep有很大概率拿到最新数据。
比如卖家发布商品,商品发布后用Ajax直接把客户端输入的内容作为新的商品显示在页面,这样卖家通过显示就已经确认产品发布成功,等到再刷新页面去查看商品,其实已经过了一段时间,也就达到了sleep的目的。
该方案的问题是不精确:
判断主备无延迟方案
确保备库无延迟,通常有三种做法。
第一种确保主备无延迟的方法是,每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0,如果不等于0就等到这个参数变成0才能执行查询请求。
第二种是对比位点。比如有一个show slave status结果的部分截图:
如果两组值完全相同,就表示接收到的日志已经同步完成。
第三种是对比GTID集合确保主备无延迟:
-
Auto_Position=1,表示这对主备关系使用了GTID协议;
-
Retrieved_Gtid_Set,是备库收到的所有日志的GTID集合;
-
Executed_Gtid_Set,是备库所有已经执行完成的GTID集合。
如果这两个集合相同,表示备库接收到的日志都已经同步完成。
这些方法相比sleep准确度提升了很多,但是还是没达到精确的程度。
接下来看看为什么说没达到精确。先回顾一个事务的binlog在主备库间的状态:
-
主库执行完成,写入binlog并反馈给客户端;
-
binlog被从主库发给备库,备库接收;
-
备库执行binlog完成。
上面的方法判断主备无延迟的逻辑是备库收到的日志都执行完成,但是有些日志处于客户端已经收到提交确认,而备库还没收到的状态,如下图:
主库上执行完成三个事务trx1、trx2、trx3,其中:
如果此时在从库B上执行查询请求,按照上面方法,从库认为没有同步延迟,但实际上查不到trx3,严格说就是出现了过期读。
配合semi-sync
要解决上面的问题,就要引入半同步复制semi-sync replication。
semi-sync做了设计:
即所有给客户端发送过确认的事务,都确保了备库已经收到这个日志。
semi-sync加位点判断的方案,能避免过期读。但该方案只对一主一备的场景成立,在一主多从场景中,主库只要等到一个从库的ack就开始给客户端返回确认,此时在从库查询就有两种情况:
判断同步位点方案还有另一个潜在问题:如果在业务更新的高峰期,主库位点或者GTID集合更新很快,那么上面两个位点等值判断就会一直不成立。
实际上,当发起一个查询请求后,要得到准确的结果,其实并不需要等到主备完全同步。比如下面的时序图:
上图中,从状态1到状态4,一直处于延迟一个事务的状态,那么如果按上面必须等到无延迟才能查询的方案,select语句直到状态4都不能被执行。但客户端是在发完trx1更新后发起的select语句,只需要确保trx1执行完就可以select了,即状态3执行查询请求其实就能获得预期结果了。
总结semi-sync配合判断主备无延迟的方案存在的问题:
接下来介绍等主库位点方案,可以解决这两个问题。
等主库位点方案
先介绍一条命令:
select master_pos_wait(file, pos[, timeout]);
该命令的逻辑为:
该命令正常返回结果是一个正整数M,表示从命令开始执行,到应用完file和pos表示的binlog位置执行了多少事务。此外还有其他结果:
那么对于之前先执行trx1再执行一个select的逻辑,要保证能查到正确数据,可以使用的逻辑:
-
trx1事务更新完成后,马上执行show master status得到当前主库执行到的file和position;
-
选定一个从库执行select;
-
在从库上执行select master_pos_wait(file, position, 1);
-
如果返回值大于等于0,则在这个从库执行查询语句;
-
否则,到主库执行查询。
整个流程为:
这里假设这条select最多在从库上等待一秒,那么如果一秒内master_pos_wait返回一个大于等于0的整数,就确保从库上执行的查询结果包含trx1的数据。
最后一步到主库执行查询,是这类方案常用的退化机制,因为不能无限等待从库。
GTID方案
如果数据库开启了GTID模式,对应的也有等待GTID的方案。
MySQL提供了一个类似的命令:
select wait_for_executed_gtid_set(gtid_set, 1);
其逻辑为:
在前面等位点的方案中,执行完事务后,还要主动去主库执行show master status。而MySQL 5.7.6版本开始,允许在执行完更新类事务后,把这个事务的GTID返回给客户端,这样等GTID的方案就可以减少一次查询。
此时执行流程变为:
流程图:
为了让第一步MySQL执行完事务在返回包带上GTID,需要将参数session_track_gtids设置为OWN_GTID,然后通过API接口mysql_session_track_get_first从返回包解析出GTID的值。
来源:https://www.cnblogs.com/san-mu/p/19054317 |