电商系统:读写分离那档子事

264 阅读10分钟

微信公众号:欢少的成长之路

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

介绍

大家好,我是Leo,目前在常州从事Java后端工程师。上篇文章我们介绍了主库,从库,从库延迟,主库挂了,从库谋权篡位那点事情。从上述中延伸了并行复制策略的发展史,切换策略等。

今天我们介绍一下读写分离那些问题,主要从概念,目的,单到多的演变,安全性演变以及六个解决方案。


思路

根据读者和用户的反馈,画了一个写作思路图。通过此图可以更好的分析出当前文章的写作知识点。可以更快的帮助读者在最短时间内判断是否为有效文章!

案例

首先就是介绍一下我个人全权负责的电商系统。这个是私活,巴西那边的线上订货平台。

这个就是客户端,用户的下单界面。

言归正传,为什么要举这个例子呢? 就是想通过电商系统,来介绍读写分离的问题。以及业务间的考量!根据业务的轻重程序选择相应的解决方案。

读写分离

是什么

读写分离就是为了提升性能压力做的一种方案。这种方案的优点与目标就是。主写,从读。专业的库做专业的事情。这样可以提供查询性能,也可以提升写的性能。同时缓解服务器的压力。目的:缓解主库的压力

单到多的裂变

这个大家还是比较熟悉的,以前的单应用时代。因为数据量不大,访问量也不大。往往一台服务器一个数据库就足矣支持日常是使用了。随着大数据时代的到来,用户量,访问量暴增,导致单库无法满足日常需求。所以数据库进行扩展。如下图就是从单库到多库裂变的示例图

安全性的裂变

随着后面科技的发展。数据的安全性能,也是人家关心的一个重点。所以代理层的概念出来了

proxy代理层优缺点

  • 有代理层:中间多了一层过滤,查询性能比没有代理层的慢一些,整体结构相对变的复杂一些。对客户端,服务端比较友好,但是对开发团队的能力要求更高。
  • 无代理层:没有代理层,中间直连,查询性能比有代理层略好一些。整体架构简单,排查问题也方便。但是在部署的时候比较繁琐,会出现主备切换,库迁移等问题。都需要动数据库连接配置。

抉择: 剩下的抉择就取决于各个公司的科技实力了!

问题

既然考虑到主从库的问题,那么必要会遇到,主库,从库数据一致性的问题。除了数据同步的一致性问题,还有应用时的一致性问题。

数据同步的一致性问题,前几篇文章我们已经讲过了。这里简单回顾一下,就不做详细介绍了。数据同步主要通过binlog完成。深入到细节的话,可以深挖binlog的三种格式,row格式,statement格式,mixed格式。三种格式各有各的优点。

  1. row:数据比较详细,但是如果数据量非常大的时候比如delete from history 这个时候要记录,删除的每一条记录,所以比较占内存
  2. statement:数据不够详细,容易导致,数据不一致 (索引引起的查询方式的不同,limit取值会不一致)
  3. mixed:上面的融合版。他中间会有一个判断,判断是否会引起数据不一致这个问题,如果是采用row,如果不会采用statement

应用时的一致性问题,最上面的电商的例子。我在管理端添加一个数据的时候,我们肯定是要第一时间查询是否添加成功的。那么我们在数据库中操作的流程是这样的。

  1. 写入数据到主库
  2. 等待主库数据同步到从库

我们在查询的时候是查的从库,所以会有一段时间的不一致问题!我们称之为 过期读 !那么我们如何处理?

解决方案

强制走主库

第一种方式就是强制走主库,强制走主库又分两条路线。不可能全部的请求都打到主库上。所以这里会有一个判断过滤。

这个判断主要会处理看当前的请求是否是及时查看的数据,就比如上述的电商商品一样,添加完商品我们肯定是要查看是否添加成功的。如果是这样的话,那肯定是要强制走主库查询的,

如果是查询一些历史数据,比如说几个月开外的数据,那么肯定不能走主库查询。所以这个就可以不强制走主库。就算晚几秒中看到也是情有可原的。用户还会自动帮我们强刷一下界面呢

sleep方案

这个方案的处理方式就是读从库之前先sleep一下。类似于select sleep(1) 。因为大概的主从库的延迟一般都是1秒,所以我们这里也是给他睡眠1秒。

直接睡一秒严格意义上是影响性能的! 重点来了

我们采用的不是睡完一秒之后,再去从库查询,而且通过前端缓存的方式。以管理端发布商品为例,直接把用户输入的新商品显示在界面上,而不是真的去数据库中查询。等下次刷新的时候也就过了sleep的时间了。主从库的数据也就同步过来了。

自然不是那么简单!

如果主从同步的时间,也就是延迟超过了1秒。那么还会出现过期读的情况

如果查询数据的时间是0.3秒,那么用户还需要等1秒。

判断主从延迟

sleep方案显然是解决不了真实的需求的。但是可以解决大部分场景,对要求比较高的公司还是无法入手。

第一种方式 就是判断主从的延迟情况。前几篇文章我们提到了使用seconds_behind_master 参数,来衡量主从库延迟的长短。

所以,我们在查询的时候可以先判断一下这个参数是否已经等于0。如果不等于0,那就必须等他等于0才可以继续操作。

第二种方式就是判断当前日志的读取位点。这里介绍两个参数。Master_Log_FileRead_Master_Log_Pos 表示的是读到主库的最新位点。以及Relay_Master_Log_FileExec_Master_Log_Pos,表示的是从库执行的最新位点。有了主库的最新位点与从库的最新位点。如果完全相同,就说明已经同步完成了。

第三种方式是对GTID的判断

谈到GTID,我们要聊到它的三个参数,第一个是是否使用协议,第二个是日志的集合,第三个是已经执行完的集合

Auto_Position=1 ,表示这对主从关系使用了 GTID 协议。

Retrieved_Gtid_Set,是从库收到的所有日志的 GTID 集合;

Executed_Gtid_Set,是从库所有已经执行完成的 GTID 集合。

如果集合相同表示同步完成

Semi-sync

seim-sync这个也是MySQL的半同步复制,基于默认的异步复制和完全同步复制之间,它是在master在执行完客户端提交的事务后不是立刻返回给客户端,而是等待至少一个slave收到并写到relay log中才返回给客户端。相对于异步复制,semisync提高了数据的安全性,但是又比完全同步性能好,所以master和slave之间的时间一定要一致,以免造成semisync失败。

这里semi-sync概念来自于 www.linuxidc.com/Linux/2017-…

流程是什么呢

  1. 事务提交的时候,主库会给从库发一个binlog日志
  2. 从库接收到binlog之后,会给主库回一下ACK包,表示已经收到了
  3. 主库收到ACK之后就表示事务完成

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

semi-synv配置前面关于位点的判断,就能确定在从库上执行的查询请求可以避免过期读。但是无法解决一主多从的问题。

有的小伙伴可能就会问了,为什么

根据上述流程我们讨论一下。主库给从库发送binlog的时候,从库收到就会给主库发ack,主库收到就完成了。那么谁管你是几个从库? 答案不就出来了嘛!

有哪些问题

  1. 一主多从的时候,在某些从库执行查询请求会存在过期读的现象;
  2. 在持续延迟的情况下,可能出现过度等待的问题。(GTID集合的位点与主库的位点永远不一致)

等主库位点方案

理解这种方案,我们要先了解一个指令

select master_pos_wait(file, pos[, timeout]);

这条指令是在从库中执行的。第一个参数 file是文件的名称,第二个参数pos是位置信息,第三个参数是表示这个函数最多可以等待多少秒。

执行完成之后,会返回一个正整数和一些结果。

正整数: 表示从开始应用执行到最后一共执行了多少事务。

结果: 如果执行期间,备库同步线程发生异常,则返回 NULL。如果等待超过 N 秒,就返回 -1。如果刚开始执行的时候,就发现已经执行过这个位置了,则返回 0。

我们可以试想一下这种方法应用之后的流程。

  • 事务执行之后,执行 show master status 获取主库上的file文件和pos位置。
  • 选定一个从库,在从库上执行 select master_pos_wait(file, pos[, timeout]); 查询返回结果
  • 如果返回的结果>0 ,说明从库已经执行过了事务的同步。可以从主库上查询数据
  • 否则就从主库,因为从库此时没有更新数据,要从主库上查。

GTID 方案

既然有了GTID模式,那必然也是有GTID方案。

下面我们就来聊一下这个方案。

这个方案比上一个等主库位点方案做了一些优化性的处理。主要优化在show master status 上。

首先介绍一下指令

select wait_for_executed_gtid_set(gtid_set, 1);
  • 逻辑就是,等待直到这个库执行的事务中包含传入的gtid_set,返回0
  • 否则就是超时返回1

在主库位点方案中,我们要多做一次查询 show master status ,而GTID帮我们省略了这一次查询。

GTID逻辑流程

  1. 事务A更新完成之后,从这个返回包中取这个事务的GTID,我们先记为gtid1
  2. 选定一个从库执行查询语句
  3. 在从库上执行上述的指令select wait_for_executed_gtid_set(gtid_set, 1);
  4. 如果返回0表示,表示从从库上查询数据,否则就从主库中(超时了)。

我们再来分析一下流程中的一些问题。返回包中取GTID,那么如何把GTID放进去呢?

只需要将参数 session_track_gtids 设置为 OWN_GTID,然后通过 API 接口 mysql_session_track_get_first 从返回包解析出 GTID 的值即可。

总结

如果觉得还能看得过去的,麻烦来个一键三连。 点赞 + 分享 + 在看

今天介绍了读写分离的一些相关概念,以及发展史,安全问题。最核心的还是读写分离给我们带来的问题,以及这个问题对应的几种方案的比较!根据特定的业务场景选择合适的方案