Redis 攻略面经(六)-- 缓存实践中的问题
📣 大家好,我是Zhan,一名个人练习时长一年半的大二后台练习生🏀
📣 这篇文章是复习 Redis 缓存实践中的问题 的学习笔记📙
📣 如果有不对的地方,欢迎各位指正🙏🏼
📣 与君同舟渡,达岸各自归🌊
👉引言
在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用
redis
做一个缓冲操作,让请求先访问到redis
,而不是直接访问Mysql
。这样可以大大缓解数据库的压力。
Redis最常用的一个场景就是作为缓存,本文主要探讨Redis作为缓存,在实践中可能会有哪些问题?比如一致性, 穿击, 穿透, 雪崩等
❄ 缓存雪崩
缓存雪崩:当大量的缓存数据在同一时间过期,或者是Redis 宕机,那么就会有大量的用户请求无法在Redis
中处理,于是全部的请求都直接打到了数据库,有可能导致数据库宕机
可以发现:缓存雪崩有两个原因,针对于这两个原因,我们应对的策略也有所不同:
🗑 大量数据同时过期
- 均匀设置过期时间:如果要给缓存数据设置过期时间,应该要避免把大量的数据设置为同一个过期时间,我们可以给数据设置过期时间时,加上一个随机数,防止数据在同一时间大量过期。
- 互斥锁:当发现访问的数据不在Redis 中时,给这个数据加上一个互斥锁,保证同一时间只有一个请求来构建缓存,构建完成再释放缓存,而获取互斥锁失败的线程,要么阻塞到构建缓存成功,要么就先返回一个空值。当然,要给互斥锁加上一个超时时间,防止一直阻塞。
- 双 key 策略:可以对缓存数据使用两个key:主key 和 备key,其中
主key
有设置过期时间,而备key
并没有设置过期时间,如果主key
过期就去读取备key
的数据,两个key的值是相等的,在更新数据的时候也会同时更新两个key的值 - 后台更新缓存:缓存不设置过期时间,靠内存淘汰策略去淘汰,然后更新缓存就交给后台线程定时更新:当业务线程发现缓存数据失效,也就是被内存淘汰策略淘汰后,就会通过消息队列发送一条消息,让后台线程更新缓存,后台线程如果发现缓存不存在就读取数据库数据并把数据加载到缓存中。
🛢 Redis 宕机
- 构建 Redis 缓存高可靠集群:如果 Redis 不宕机就不会存在这个问题,因此我们可以以集群保证Redis 的高可用
- 服务熔断:如果确确实实Redis宕机了,我们可以启用服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,等待Redis 恢复正常再正常进行业务应用的访问
- 请求限流:在Redis 宕机后,我们可以只把少部分请求发送到数据库进行处理,限制请求的数量,等待Redis 恢复正常并进行完缓存预热后再正常进行业务应用的访问
🎯 缓存击穿
例如在商品抢购的场景,商品信息这些东西我们会做缓存,因为用户会频繁访问这个key,我们称为热点key
缓存击穿:如果缓存中某个热点key
过期了,大量的请求无法直接从缓存中获取数据,只能访问数据库,数据库经受不起高并发的请求,导致宕机,也就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
针对缓存击穿,我们有两种方案去解决,两种方案都是缓存雪崩中的方案:
- 互斥锁:保证同一时间只有一个线程拿到互斥锁,并进行数据缓存的构建,此时没有拿到互斥锁的线程返回空值,或者阻塞直至互斥锁释放
- 后台异步更新缓存:不给热点key设置过期时间,由后台异步更新缓存,或者在热点key过期前提前通知后台线程,后台线程更新缓存,并重设过期时间
📌 缓存穿透
前面讲到的缓存雪崩和缓存击穿,都是数据库中存在数据,但是缓存失效打到数据库,缓存穿透则是数据库和缓存中都没有数据
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
一般发生缓存穿透的情况有:
- 黑客的恶意攻击:故意访问那些业务数据不存在的业务
- 业务误操作:由于误操作把数据库和缓存的数据都删除了,导致缓存中和数据库中都不存在
解决缓存穿透的方案,常见的有以下三种:
- 限制非法请求:我们有提到,黑客恶意访问数据不存在的业务会造成缓存穿透,但是如果我们做好参数的校验,对于恶意请求直接返回错误,就能避免缓存穿透
- 缓存空对象:当我们发现数据库中不存在时,就缓存中设置一个空值,后续再来查询的时候返回就是空值,就不会打到数据库去了
- 布隆过滤器:布隆过滤器介于客户端和Redis之间,可以判断数据是否存在,如果不存在就能直接拒绝访问,如下图:
那布隆过滤器如何判断数据是否存在的呢?
布隆过滤器由初始值都为0的位图和N个哈希函数两部分组成,在插入数据的时候会在位图中做标记表示数据存在,后续就可以判断数据是否存在,具体如何标记和判断的我们以下面这个例子来了解:
假设存在数组长度为8的位图和3个哈希函数的布隆过滤器,现插入数据 record
,它的标记过程就为:
- 使用3个哈希函数计算出
record
的哈希值 - 然后用哈希值对
8
取余,得到三个下标 - 把位图上对应的三个下标的值修改为1
而我们判断一个数据record
是否存在的过程就为:
- 使用3个哈希函数计算出
record
的哈希值 - 然后用哈希值对
8
取余,得到三个下标 - 判断三个下标的值是否为1,如果有一个不为1,那么就不存在
其实我们可以发现,尽管索引为1,4,6
的都是1,但是不一定这个数据存在,可能索引为1
的1是record2
留下的,而record3
留下了索引为4,6
的1,实际上record
不存在
因此:布隆过滤器判断数据存在不准确,但是判断数据不存在就一定不存在
🧲 缓存与数据库的一致性
由于引入了缓存,那么在数据更新的时候,不仅要更新数据库,还要更新Redis中的缓存数据,那么先做更新数据库,还是先做更新缓存呢?
🔴先更新数据库,再更新缓存
如果先更新数据库删除缓存,再更新缓存,就会发生如下这种情况,也就是所谓的并发问题,最后导致数据库中的数据和缓存的数据不一致
🟢 先更新缓存,再更新数据库
如果先更新缓存,然后更新数据库中的数据,同样也会发生下图的并发问题,最后会发现数据库和缓存中的数据并不相同:
其实我们可以发现:无论是先更新哪个,都会存在并发问题,可能导致数据库与缓存的数据不一致的情况
- 对于一个写多读少的业务场景,如果是采用更新缓存的策略,那么就会出现,缓存数据没有被读到,但是缓存数据一直在更新,造成性能损耗
- 而且如果存入 Redis 缓存的不是直接从数据库拿来的数据,而是要经过计算,也就是数据库中读取的数据需要进行加工再存入Redis,那么也会造成许多没有必要的性能损失
因此,我们最后并不采用更新缓存的方法,而是删除缓存,当第二次读取发现缓存数据不存在的时候,就选择去更新缓存。
对于缓存命中率要求比较高的业务,也就是说必需更新缓存的业务,那我们只能在更新缓存的时候加上互斥锁,或者给更新的缓存数据的过期时间设置短一些,这样,即使是脏数据,也不会太久。
那么又出现了一个问题,先删缓存还是先删数据库呢?
🟡 先删缓存,再更新数据库
如果先删除缓存,再更新数据库,也会出现并发问题,出现数据库中的数据和缓存的数据不一致的现象:
🟠 先更新数据库,再删缓存
只剩下“先更新数据库,再删缓存”了,可事实上,它也存在并发问题,不过,它的并发问题发生几率很低,具体原因我们一起来看看
这是发生并发问题的场景,我们发现发生并发问题的前提是:B 线程修改数据库的耗时远远低于 A 去查询数据库得到值的耗时,但是显然写操作的耗时会比读操作的耗时更长,但是很明显,缓存的写入速度通常要远远快于数据库的写入速度,因此发生这种并发问题的几率很低,而且我们还能给更新的缓存加上一个过期时间作为兜底方案。
🔊 删除缓存失败了?
上面的方法看起来万无一失了,但是问题又来了,如果更新完数据库,删除缓存这一步失败了呢?那么读取到的就是缓存没有更新的旧数据
也就是上述这种情况,由于删除缓存失败导致数据库和缓存的数据不一致,针对于这个问题,我们有以下两种解决方法:
📢 重试机制
我们可以使用消息队列,把要删除的数据丢入消息队列:
- 如果删除缓存失败,就会消息队列中重新读取数据,然后重新删除缓存,也就是所谓的重试机制,如果重试次数过多仍然消息存在,就要向业务层报错了
- 如果删除缓存成功,就删除消息队列这条信息
📣 订阅 binlog
订阅binlog这种做法其实有点类似于主从复制,我们可以使用中间件,如阿里的canal:
- canal会模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点
- 向 MySQL 主节点发送
dump
请求 - MySQL 收到请求后,就会开始推送
Binlog
给 Canal - 这样就算订阅
MySQL的binlog
这样MySQL进行数据的写入、删除、更新的操作,都能被canal监听到,然后Redis就根据binlog
中的数据,对Redis进行更新
💬 总结
本文讲了Redis缓存中可能出现的四个问题:缓存雪崩、缓存击穿、缓存穿透、缓存与数据库的一致性:
- 对于缓存雪崩:我们提到了缓存雪崩的两个原因,以及针对这两个原因的解决方法
- 对于缓存击穿:其实发现缓存击穿和雪崩有一些相似之处,因此解决缓存击穿的方法来自于缓存雪崩
- 对于缓存穿透:不同于缓存雪崩和击穿,数据库和缓存中都没有数据,我们同样给出了三个解决方案,并对解决方案中的布隆过滤器进行了原理的讲解
- 对于一致性:尽管缓存能够减少数据库的压力,但同时我们也要保障数据的一致性,不能出现脏数据,因此我们展开了如何保证数据库与缓存的一致性的讨论 相信读完今天的文章,大家能对Redis 缓存实践中可能出现的问题,以及解决方案有了更深一步的理解~
🍁 友链
- Redis 攻略面经(一)-- 常见数据类型
- Redis 攻略面经(二)-- 关于持久化,你了解多少?
- Redis 攻略面经(三)-- 详解内存回收的两种策略
- Redis 攻略面经(四)-- 详解主从复制中的同步数据
- Redis 攻略面经(五)-- 详解哨兵机制
✒写在最后
都看到这里啦~,给个点赞再走呗~,也欢迎各位大佬指正以及补充,在评论区一起交流,共同进步!也欢迎加微信一起交流:Goldfish7710。咱们明天见~
转载自:https://juejin.cn/post/7175460872046706743