之前说的一主多从结构其实就是主从分离的基本结构!读写分离主要分摊主库的压力,
如上图结构是客户端主动做负载均衡,这种模式下一般会把数据库的连接信息放在客户端的连接层。也就是说由客户端来选择后端数据库进行查询。
这种架构是,在MySQL和客户端之间有一个中间代理层proxy,客户端只连接 proxy,由proxy根据请求类型和上下文决定请求的分发路由。
这两种读写分离各有哪些特点:
1.客户端直连:中间没有其他中间价转发,所以性能稍微好点,并且因为整体架构简单,所以排查问题方便,但是如果出现问题,需要主从切换,因为客户端都会感知,则需要调整数据库连接信息,虽然听起来很麻烦,赶紧架构比较丑,一般采用这 样的架构,一定会伴随一个负责管理后端的组件,比如Zookeeper,尽量让业务端只专注于 业务逻辑开发。
2.带proxy的架构,对客户端比较友好,因为维护操作都是proxy处理的,但是这样的话运维团队的要求比较高,而且,proxy 也需要有高可用架构。因此,带proxy架构的整体就相对比较复杂。
理解了这两种方式的优缺点,运维团队就可以根据自身情况选择合适的方案了,但目前看优势是proxy的架构方向的,但是不论哪种方式,都会碰到主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果此时查询的是从库的话,就有可能读到刚刚的事务更新之前的状态。
这种“在从库上会读到系统的一个过期状态”的现象,在这篇文章里,我们暂且称之为“过期 读”。
处理过期读有哪些方案呢?
这个方案其实是将查询请求做分类,通常情况下,我们可以将查询请求分为两类:
1.对于必须要拿到的结果,就强制将其发到主库上。
2.对于可以读到旧数据的请求,才将其发到从库上。
这个方案是用的最多的,哈哈,但是该方案最大的问题是遇到那些 可能查询都得需要最新的数据的需求吗,此时相当于放弃了读写分离,所有的读写的压力都在主库,等于放弃了扩展性。
那么在讨论下可以支持读写分离的场景下,有哪些解决过期读的方案,
这个方案顾名思义,主库更新完,查询从库时先睡眠一下,类似执行了一条 select sleep(1)命令,这个方案的假设是大部分的主备延迟在一秒之内,所以做一个sleep可以很大概率拿到最新的值。
但是感觉有点不靠谱,它也可以这样使用,以卖家发布商品为例,商品发布后,用Ajax(Asynchronous JavaScript + XML,异步JavaScript 和XML)直接把客户端输入的内容作为“新的商品”显示在页面上,而不是真正地去数据库做查询。
这样,卖家就可以通过这个显示,来确认产品已经发布成功了。等到卖家再刷新页面,去查看商 品的时候,其实已经过了一段时间,也就达到了sleep的目的,进而也就解决了过期读的问题。
sleep方案能够解决当前这样的问题,但是,严格来说很不精确,一个是如果查询0.5秒就可以拿到正确结果,但是现在需要等待一秒,2是延迟超过一秒之后的话,此时查询还是会查询过期的值。
咱们看一下比上面更准确的方案
要确保备库无延迟,通常有三种做法。
我们知道show slave status结果里的seconds_behind_master参数的 值,可以用来衡量主备延迟时间的长短。所以判断里第一种方案就是从库执行查询时,先判断seconds_behind_master是否已经等于0。如果还不等于0 ,那就必须等到这个参数变为0才能执 行查询请求。
当然还可以采用对比位点和GTID,的方法来确保主备无延迟,也就是我们接下来要说的第二和第三种方法。
如图所示show slave status结果的部分截图。
现在我们就来看看对比位点和GTID确保主备无延迟
对比位点确保主备无延迟:
如果Master_Log_File和Relay_Master_Log_File、Read_Master_Log_Pos和Exec_Master_Log_Pos这两组值完全相同,就表示接收到的日志已经同步完成。
对比GTID集合确保主备无延迟:
如果这两个集合相同,也表示备库接收到的日志都已经同步完成。
可见,对比位点和对比GTID这两种方法,都要比判断seconds_behind_master是否为0更准确。
在执行查询请求之前,先判断从库是否同步完成的方法,相比于sleep方案,准确度确实提升了 不少,但还是没有达到“精确”的程度。为什么这么说呢?
你想在业务的高峰期时,则会出现主库的位点或者GTID集合更新很快,那么上面的两个位点等值判断就会一直不成立,很可能出现从库上 迟迟无法响应查询请求的情况。
还有一种就是主库执行完以后刚要发给备库,备库还没有收到,如下图
这时,主库上执行完成了三个事务trx1、trx2和trx3,其中:
怎么解决此问题呢?
引入半同步复制,也就是semi-sync replication。
semi-sync做了这样一个设计:
也就是说,如果启用了semi-sync,就表示所有给客户端发送过确认的事务,都确保了备库已经收到了这个日志。
但是,semi-sync+位点判断的方案,只对一主一备的场景是成立的。在一主多从场景中,主库只 要等到一个从库的ack,就开始给客户端返回确认。这样如果在一主多从,如果刚好查询到给ack回馈的话数据正确,如果不是则还是过期读。
要理解等主库位点方案,我需要先和你介绍一条命令:
select master_pos_wait(file, pos[, timeout]);
这条命令的逻辑如下:
1.它是在从库执行的
2.参数file和pos指的是主库上的文件名和位置;
3.timeout可选,设置为正整数N表示这个函数最多等待N秒。
这个命令正常的返回结果是一个正整数M,表示从命令开始执行,到应用完file和pos表示的 binlog位置,执行了多少事务。
如果有这样的其他结果为:
1.如果执行期间,备库同步线程发生异常,则返回NULL;
2.如果等待超过N秒,就返回-1;
3.如果刚开始执行的时候,就发现已经执行过这个位置了,则返回0。
这个命令是怎么解决问题的?
1.一个更新事务完成后,马上执行showmasterstatus得到当前主库执行到的File和Position;
2.选定一个从库执行查询语句,在从库上执行select master_pos_wait(File,Position,1);
3.如果返回值是>=0的正整数,则在这个从库执行查询语句;
5.否则,到主库执行查询语句。
假如查询最多,那么一秒内master_pos_wait返回一个大于等于0的整数,就确保了从库上执行的这个查询结果就正确,但是超过1秒还是会走从库,这种方案都是退化机制,从库延迟时间不可控,不可能无限等待下去,所以如果等待超时,就应该放弃,然后到主库去查。
按照我们设定不允许过期读的要求,就只有两种选择,一种是超时放弃,一种是转到主库 查询。具体怎么选择,就需要业务开发同学做好限流策略了。
如果你的数据库开启了GTID模式,对应的也有等待GTID的方案。
MySQL中同样提供了一个类似的命令:
select wait_for_executed_gtid_set(gtid_set, 1);
这条命令的意思是:
1. 等待,直到这个库执行的事务中包含传入的gtid_set,返回0;
2. 超时返回1。
在前面等位点的方案中,我们执行完事务后,还要主动去主库执行show master status。而 MySQL 5.7.6版本开始,允许在执行完更新类事务后,把这个事务的GTID返回给客户端,这样 等GTID的方案就可以减少一次查询。
等GTID的执行流程如下:
1.事务更新完成后,从返回的包直接获取这个事务的GTID,记为gtid1;
2.选定一个从库执行查询语句;在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);
3.如果返回值是0,则在这个从库执行查询语句;
4.否则,到主库执行查询语句。
跟等主库位点的方案一样,等待超时后是否直接到主库查询,需要业务开发同学来做限流考虑。
感谢林晓斌老师的《MySQL 实战 45 讲》,讲的非常精彩,本文是其学习笔记以及概括版!