文章目录
一、前述
关于Redis缓存的击穿、穿透和雪崩应该是再熟悉不过的词了,也是面试常问的高频试题。
不过,对于这三大缓存的问题,有很多人背过了解决方案,却少有人能把思路给理清的,首先大致画了一个系统的简略架构图。
二、缓存穿透
1、简单介绍
缓存穿透是指查询一个根本不存在的数据,缓存层和持久层都不会命中。在日常工作中出于容错的考虑,如果从持久层查不到数据则不写入缓存层,缓存穿透将导致不存在的数据每次请求都要到持久层去查询,失去了缓存保护后端持久的意义~
流程图
正常情况下问题并不大,但是如果用户恶意重复请求在 Redis 和 DB 中都不存在的资源。那么每次请求都会直接打到 DB(Mysql) 上,严重时可能会导致物理 DB(Mysql) 宕机。
2、 解决方案
2.1- 用户请求合法性校验
对用户的请求合法性进行校验,拦截恶意重复请求。
2.2- 缓存空结果
如果系统发现 Redis 及 DB 中都不存在该资源,就缓存空结果一段时间。
注意:空结果的失效时间不能设置的太长,否则数据的实效性会产生很大的问题。
2.3- 布隆过滤器
(1)、布隆过滤器,英文叫BloomFilter,可以说是一个二进制向量和一系列随机映射函数实现。 可以用于检索一个元素是否在一个集合中。 在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。
(2)、算法描述:
- 初始状态时,BloomFilter是一个长度为m的位数组,每一位都置为0。
- 添加元素x时,x使用k个hash函数得到k个hash值,对m取余,对应的bit位设置为1。
- 判断y是否属于这个集合,对y使用k个哈希函数得到k个哈希值,对m取余,所有对应的位置都是1,则认为y属于该集合(哈希冲突,可能存在误判),否则就认为y不属于该集合。可以通过增加哈希函数和增加二进制位数组的长度来降低错报率。
注意:布隆过滤器的原理还是比较简单的。这里我们需要注意,布隆过滤器可能存在一定误判的可能性,但它依然可以帮助你拦截掉大部分一定不存在的数据。
(3)、案例分析
假设我们现在有一个长度为 9 的 bit 数组,该数组的每个位置上只能保存 1 或者 0,1 标识该位置被占用,0 标识该位置未被使用。
Key值 | 取模后的值 |
---|---|
Key1 | 0、3、5 |
Key2 | 1、4、6 |
Key3 | 5、7、8 |
最后,我们会发现这个 bit 数组里只有位置 2还是空着的。如果此时来了一个新的 key4 通过三个Hash算法求出的哈希值为 1、2、4,我们则可以断定 key4 一定不存在。
二、缓存击穿
1、简单介绍
是针对缓存中没有但数据库有的数据。场景是,当Key失效后,假如瞬间突然涌入大量的请求,来请求同一个Key,这些请求不会命中Redis,都会请求到DB,导致数据库压力过大,甚至扛不住,挂掉。
案例:当前key是一个热点key(例如一个秒杀活动),并发量非常大,当秒杀活动开始的时候,Redis 集群中数据在此刻正好过期了,那么无数的请求则直接打到了秒杀系统的物理 DB 上,DB 瞬间挂了~
2、 解决方案
2.1- 热点数据永远不过期
- 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。
- 从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去更新缓存。
2.2- 分布式互斥锁
发现没有命中Redis,去查数据库的时候,在执行更新缓存的操作上加锁,只允许一个线程重建缓存,其它线程等待重建缓存的线程执行完,这个执行更新缓存线程访问过后,缓存中的数据会被重建,这样其他线程就可以从缓存中取值。
3、 两种方案对比
- 分布式互斥锁:这种方案思路比较简单,但是存在一定的隐患,如果在查询数据库 + 和 重建缓存(key失效后进行了大量的计算)时间过长,也可能会存在死锁和线程池阻塞的风险,高并发情景下吞吐量会大大降低!但是这种方法能够较好地降低后端存储负载,并在一致性上做得比较好。
- “永远不过期”:这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。
三、缓存雪崩
1、简单介绍
是指Redis集群中大量热点Key同时失效,对这些Key的请求又会打到DB上,同样会导致数据库压力过大甚至挂掉。
2、 解决方案
2.1- 让Key的失效时间分散开,可以在统一的失效时间上再加一个随机值,或者使用更高级的算法分散失效时间。
2.2- 构建多个redis实例,个别节点挂了还有别的可以用。
2.3- 多级缓存:比如增加本地缓存,减小redis压力。
2.4- 对存储层增加限流措施,当请求超出限制,提供降级服务(一般就是返回错误即可)。
四、Redis跳跃表
这一段时间看Redis的时候看到了跳跃表方面的问题,就顺便总结一下
1、什么是跳跃表?
跳表是一个随机化的数据结构,实质是一种可以进行二分查找的有序链表。跳表在原有的有序链表上增加了多级索引,通过索引来实现快速查询。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。跳跃表的效率可以和平衡树想媲美了,最关键是它的实现相对于平衡树来说,代码的实现上简单很多~
跳跃表在 Redis 中使用不是特别广泛,只用在了两个地方。一是实现有序集合键,二是集群节点中用作内部数据结构~
2、跳跃表原理图解
2.1-来自《Redis 设计与实现》这本书中的一张完整的跳跃表的图。
2.2- 跳跃表的 level 是如何定义的?
跳跃表 level 层级完全是随机的。一般来说,层级越多,访问节点的速度越快。
2.3- 跳跃表的插入
1)插入数据之前,链表是空的,如下图:
2)插入 level = 3,key = 1
3)插入 level = 1,key = 2
4)插入 level = 2,key = 3
5)插入 level = 3,key = 5
6)插入 level = 1,key = 66
7)插入 level = 2,key = 100
2.4- 跳跃表的查询
跳跃表的查询是从顶层往下找,那么会先从第顶层开始找,方式就是循环比较,如过顶层节点的下一个节点为空说明到达末尾,会跳到第二层,继续遍历,直到找到对应节点。
现在我们要找键为 66 的节点的值。那跳跃表是如何进行查询的呢?
1)如下图所示红色框内,我们带着键 66 和 1 比较,发现 66 大于 1。继续找顶层的下一个节点,发现 66 也是大于五的,继续遍历。由于下一节点为空,则会跳到 level 2。
2)上层没有找到 66,这时跳到 level 2 进行遍历,但是这里有一个点需要注意,遍历链表不是又重新遍历。而是从 5 这个节点继续往下找下一个节点。如下,我们遍历了 level 3 后,记录下当前处在 5 这个节点,那接下来遍历是 5 往后走,发现 100 大于目标 66,所以还是继续下沉。
3)当到 level 1 时,发现 5 的下一个节点恰恰好是 66 ,就将结果直接返回。
2.5- 跳跃表的删除
2.6- 跳跃表的修改
跳跃表的修改和数据结构中的链表修改类似,更改所需要修改的值即可。
五、淘汰策略
- 默认情况下,Redis 在使用的内存空间超过 maxmemory 值时,并不会淘汰数据,也就是设定的 noeviction 策略,写满后再写会返回错误。
- volatile-ttl 策略,在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
- volatile-random策略,在设置了过期时间的键值对中,进行随机删除。
- volatile-lru 策略,会使用 LRU 算法筛选设置了过期时间的键值对。最近最少使用的会被删掉。
- volatile-lfu 会使用 LFU 算法选择设置了过期时间的键值对。首先会筛选并淘汰访问次数少的数据,然后针对访问次数相同的数据,再筛选并淘汰访问时间最久远的数据。
- allkeys-random 策略,从所有键值对中随机选择并删除数据。
- allkeys-lru 策略,使用 LRU 算法在所有数据中进行筛选。
- allkeys-lfu 策略,使用 LFU 算法在所有数据中进行筛选。
六、总结
欢迎评论区交流 ~