一、背景介绍
众所周知,常用的关系型数据库MySQL底层是以B+树来组织存储在磁盘中的数据,而由于磁盘IO的读写性能较差,加之实际业务场景中读操作的次数要数倍于写操作。因此,适当的将读写操作分离,设计一种合适的缓存策略对提升MySQL性能异常重要。
本文讲述的重点放在MySQL读写分离和缓存方案上,同时介绍MySQL的主从复制原理,对缓存方案中存在的数据同步问题进行分析并给出一致性方案,最后对三类常见的缓存问题进行分析。
二、MySQL主从复制原理
MySQL的主从复制主要解决的问题是数据备份、高可用性、故障切换等,在本文中提出主要是为了进一步区分缓存策略和主从复制解决的痛点问题。
先上一张《高性能MySQL》中的图:
图中主要描述了MySQL复制的三个流程:
第一步: 在主库上记录二进制日志(Binary Log),即binlog。每次准备提交事务完成数据更新前,主库会将要更新的事件记录到binlog当中,值得注意的是,binlog中是按照事务提交的顺序来记录的,而非直观上的语句执行顺序,在记录完日之后,主库就会告诉存储引擎可以提交事务啦!
第二步: 从库将主库的binlog复制到本地的中继日志(Relay Log)。从库首先会启动一个IO线程来跟主库建立连接,接着在主库上启动一个名为“二进制转储”线程,用于读取主库上binlog中的事件,然后从库上的IO线程将接收到的事件记录到relay-log当中。
第三步: 从库上的SQL线程读取relay-log中的事件并重放执行,实现数据更新。值得注意的是,从库的IO线程和SQL线程是独立工作的,类似于生产者消费者模型,实现了获取事件和重放事件的解耦,同时也限制了复制的过程,成为了一些工作的性能瓶颈。
三、读写分离
3.1 基本架构
读写分离的结构如下图所示:
一般将缓存数据库作为辅助数据库,存放热点数据,MySQL则是主要数据库。
3.2 缓存必要性
学过《计组》的同学们都知道,内存的访问速度是磁盘访问速度的10万倍(数量级倍率),内存的访问速度大约是100ns,而一次磁盘访问大约是10ms;此外,访问mysql时访问磁盘的次数跟b+树的高度相关,因此,引入缓存意义就非常重要。
值得注意的是,MySQL内部也有缓存层来保存热点数据,其与具体业务无关,只是包括数据文件、索引文件等,这里以秒杀活动为例,区分MySQL内部缓冲池和缓存数据库。
假设12点有一个秒杀活动,11点有大量用户注册,这就会在MySQL内部缓冲池保存部分数据,但这些用户并不是活跃的,只有在12点左右才是活跃的,也就是所说的用户热点数据,一般选择将这些数据放到缓存数据库(如redis,memcached)当中,提高MySQL性能。
3.3 同步问题分析
如图3.1中的架构图所示,引入缓存后就存在数据的同步问题,对于指定数据操作时,一般存在下面几种情况:
- mysql有,缓存无
- mysql无,缓存有
- 都有,但数据不一致
- 都有,数据一致
- 都没有
显然,我们只需要考虑前三种异常情况,为此,我们对数据库操作进行调整:
方案一
- 在进行删除操作时,先删除redis数据,再删除MySQL数据。
- 在进行修改操作时,先删除redis数据,再修改MySQL数据。
- 在进行插入操作时,先删除redis数据,在修改MySQL数据。
方案二
不同于方案一操作的繁琐和效率低下,方案二采用了过期时间的概念,具体操作如下:
- 在进行删除操作时,先删除redis数据,再删除MySQL数据。
- 在修改和插入操作时,首先修改redis数据,并设置过期时间(如100ms),等修改完MySQL并同步redis时再删除过期时间。
四、缓存异常
以上考虑的都是逻辑自洽的方案,那么当缓存发生异常时应该怎么处理呢,下面我们列举几种常见的缓存异常的情况。
4.1 缓存穿透
情形
某类数据既不在redis也不在MySQL,而客户还是一直在尝试读,这时就会产生缓存穿透,数据读取的压力都集中在MySQL,可能造成MySQL不堪重负至奔溃。
解决
- 发现mysql不存在,将redis设置为 <key, nil> 并设置过期时间,下次访问key的时候就不再访问
mysql,当然这容易造成redis缓存很多无效数据。 - 采用布隆过滤器,将mysql当中已经存在的key,写入布隆过滤器,不存在的直接pass掉。(关于布隆过滤器可以看这篇:海量数据去重、HASH、布隆过滤器介绍)
4.2 缓存击穿
情形
某类数据不在redis中,而MySQL中有,此时发生对该类数据的大量并发请求时,就会造成MySQL压力过大的情况。
解决
- 加锁。请求数据的时候获取锁,如果获取成功则操作,获取失败则休眠一段时间(200ms)再去获取;获取成功后,释放锁。
此时一般的流程是:首先读redis,不存在,读mysql,存在,写redis key的锁,整个流程走完,才让后面的服务器访问。 - 将很热的key,设置不过期,即频繁使用的用户数据常驻在redis中。
4.3 缓存雪崩
情形
当redis失效时,出现redis无数据,MySQL有数据,导致请求全部走mysql,有可能搞垮数据库,使整个服务失效。
解决
缓存数据库在整个系统不是必须的,也就是缓存宕机不会影响整个系统提供服务。
- 如果因为缓存数据库宕机,造成所有数据涌向mysql。可以采用高可用的集群方案,如哨兵模式、cluster模式;
- 如果因为设置了相同的过期时间,造成缓存集中失效。可以设置随机过期值或者其他机制错开失效时间,也就是错峰策略;
- 如果因为系统重启的时候,造成缓存数据消失。若重启时间短,将redis开启持久化(过期信息也会持久化); 重启时间长则提前将热数据导入redis当中。
五、总结
本文系统的总结了MySQL缓存方案的必要性,对引入缓存后的数据同步问题进行分析,最后再对三种缓存异常情形进行总结,列举常用的解决方案,希望能帮助同学们理清思路哈!
参考
《高性能MySQL-第三版》
https://blog.csdn.net/yuan2019035055/article/details/122310447
https://blog.csdn.net/yangbindxj/article/details/123307888?spm=1001.2100.3001.7377&utm_medium=distribute.pc_feed_blog_category.none-task-blog-classify_tag-7.nonecase&depth_1-utm_source=distribute.pc_feed_blog_category.none-task-blog-classify_tag-7.nonecase