读写分离有哪些坑?

之前说的一主多从结构其实就是主从分离的基本结构!读写分离主要分摊主库的压力,

读写分离有哪些坑?_第1张图片

 如上图结构是客户端主动做负载均衡,这种模式下一般会把数据库的连接信息放在客户端的连接层。也就是说由客户端来选择后端数据库进行查询。

读写分离有哪些坑?_第2张图片

这种架构是,在MySQL和客户端之间有一个中间代理层proxy,客户端只连接 proxy,由proxy根据请求类型和上下文决定请求的分发路由。

这两种读写分离各有哪些特点:

1.客户端直连:中间没有其他中间价转发,所以性能稍微好点,并且因为整体架构简单,所以排查问题方便,但是如果出现问题,需要主从切换,因为客户端都会感知,则需要调整数据库连接信息,虽然听起来很麻烦,赶紧架构比较丑,一般采用这 样的架构,一定会伴随一个负责管理后端的组件,比如Zookeeper,尽量让业务端只专注于 业务逻辑开发。

2.带proxy的架构,对客户端比较友好,因为维护操作都是proxy处理的,但是这样的话运维团队的要求比较高,而且,proxy 也需要有高可用架构。因此,带proxy架构的整体就相对比较复杂。

理解了这两种方式的优缺点,运维团队就可以根据自身情况选择合适的方案了,但目前看优势是proxy的架构方向的,但是不论哪种方式,都会碰到主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果此时查询的是从库的话,就有可能读到刚刚的事务更新之前的状态。

 这种“在从库上会读到系统的一个过期状态”的现象,在这篇文章里,我们暂且称之为“过期 读”。

处理过期读有哪些方案呢?

1.强制走主库方案

这个方案其实是将查询请求做分类,通常情况下,我们可以将查询请求分为两类:

1.对于必须要拿到的结果,就强制将其发到主库上。

2.对于可以读到旧数据的请求,才将其发到从库上。

这个方案是用的最多的,哈哈,但是该方案最大的问题是遇到那些 可能查询都得需要最新的数据的需求吗,此时相当于放弃了读写分离,所有的读写的压力都在主库,等于放弃了扩展性。

那么在讨论下可以支持读写分离的场景下,有哪些解决过期读的方案,

2.Sleep 方案

这个方案顾名思义,主库更新完,查询从库时先睡眠一下,类似执行了一条 select sleep(1)命令,这个方案的假设是大部分的主备延迟在一秒之内,所以做一个sleep可以很大概率拿到最新的值。

但是感觉有点不靠谱,它也可以这样使用,以卖家发布商品为例,商品发布后,用Ajax(Asynchronous JavaScript + XML,异步JavaScript 和XML)直接把客户端输入的内容作为“新的商品”显示在页面上,而不是真正地去数据库做查询。

这样,卖家就可以通过这个显示,来确认产品已经发布成功了。等到卖家再刷新页面,去查看商 品的时候,其实已经过了一段时间,也就达到了sleep的目的,进而也就解决了过期读的问题。

sleep方案能够解决当前这样的问题,但是,严格来说很不精确,一个是如果查询0.5秒就可以拿到正确结果,但是现在需要等待一秒,2是延迟超过一秒之后的话,此时查询还是会查询过期的值。

咱们看一下比上面更准确的方案

3.判断主备无延迟方案

要确保备库无延迟,通常有三种做法。

我们知道show slave status结果里的seconds_behind_master参数的 值,可以用来衡量主备延迟时间的长短。所以判断里第一种方案就是从库执行查询时,先判断seconds_behind_master是否已经等于0。如果还不等于0 ,那就必须等到这个参数变为0才能执 行查询请求。

当然还可以采用对比位点和GTID,的方法来确保主备无延迟,也就是我们接下来要说的第二和第三种方法。

读写分离有哪些坑?_第3张图片

 如图所示show slave status结果的部分截图。

现在我们就来看看对比位点和GTID确保主备无延迟

对比位点确保主备无延迟:

  • Master_Log_File和Read_Master_Log_Pos,表示的是读到的主库的最新位点;
  • Relay_Master_Log_File和Exec_Master_Log_Pos,表示的是备库执行的最新位点。

如果Master_Log_File和Relay_Master_Log_File、Read_Master_Log_Pos和Exec_Master_Log_Pos这两组值完全相同,就表示接收到的日志已经同步完成。

对比GTID集合确保主备无延迟:

  • Auto_Position=1 ,表示这对主备关系使用了GTID协议。
  • Retrieved_Gtid_Set,是备库收到的所有日志的GTID集合;
  • Executed_Gtid_Set,是备库所有已经执行完成的GTID集合。

如果这两个集合相同,也表示备库接收到的日志都已经同步完成。

可见,对比位点和对比GTID这两种方法,都要比判断seconds_behind_master是否为0更准确。

在执行查询请求之前,先判断从库是否同步完成的方法,相比于sleep方案,准确度确实提升了 不少,但还是没有达到“精确”的程度。为什么这么说呢?

你想在业务的高峰期时,则会出现主库的位点或者GTID集合更新很快,那么上面的两个位点等值判断就会一直不成立,很可能出现从库上 迟迟无法响应查询请求的情况。

还有一种就是主库执行完以后刚要发给备库,备库还没有收到,如下图

读写分离有哪些坑?_第4张图片

 这时,主库上执行完成了三个事务trx1、trx2和trx3,其中:

  • trx1和trx2已经传到从库,并且已经执行完成了;
  • trx3在主库执行完成,并且已经回复给客户端,但是还没有传到从库中。

怎么解决此问题呢?

4.配合semi-sync

引入半同步复制,也就是semi-sync replication。

semi-sync做了这样一个设计:

  • 事务提交的时候,主库把binlog发给从库;
  • 从库收到binlog以后,发回给主库一个ack,表示收到了;
  • 主库收到这个ack以后,才能给客户端返回“事务完成”的确认。

也就是说,如果启用了semi-sync,就表示所有给客户端发送过确认的事务,都确保了备库已经收到了这个日志。

但是,semi-sync+位点判断的方案,只对一主一备的场景是成立的。在一主多从场景中,主库只 要等到一个从库的ack,就开始给客户端返回确认。这样如果在一主多从,如果刚好查询到给ack回馈的话数据正确,如果不是则还是过期读。

5.等主库位点方案

要理解等主库位点方案,我需要先和你介绍一条命令:

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的方案。

6.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.否则,到主库执行查询语句。

跟等主库位点的方案一样,等待超时后是否直接到主库查询,需要业务开发同学来做限流考虑。

读写分离有哪些坑?_第5张图片

感谢林晓斌老师的《MySQL 实战 45 讲》,讲的非常精彩,本文是其学习笔记以及概括版!

你可能感兴趣的:(#,MySQL,数据库)