1.上述问题造成的原因及解决方案
(1)缓存雪崩:
《1》造成原因:
简单说就是redis中的数据大量过期或失 效,当用户的大量请求访问过来时,redis已不能发挥它作为缓存的功能。即用户的请求在 redis中无法获取到需要的数据,进而这些请求同时到达数据库。我们也了解过传统数据库 (如 MySQL)的QPS并不高,不擅长处理高并发的请求,因此就会可能造成数据库崩溃。
《2》解决方案:
上面在造成原因中我们分析出是由于redis中的大量数据同时过期或失效才造成的缓存雪崩,那只要不让数据同时过期失效就可以解决这个问题了,具体解决方案如下:
《2.1》给不同的Key的TTL添加随机值
《2.2》利用Redis集群提高服务的可用性
《2.3》给缓存业务添加降级限流策略
《2.4》给业务添加多级缓存
(2)缓存击穿:
《1》造成原因:
缓存击穿主要造成的原因就是被高并发访问的热点key突然失效,造成这些高并发访问请求同时涌入数据库。
《2》解决方案:
对于缓存击穿,常用的解决方案有两种:加互斥锁和设计逻辑过期
《2.1》互斥锁:
假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么 线 程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到 锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻 辑,此时就能够从缓存中拿到数据了。具体执行逻辑如下图示:
《2.2》逻辑过期:
我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis, 而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据 已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开 启一个 新线程2去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放 锁,而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程 3 无法 获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他 线程才能走返回正确的数据。这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存 之前,返回的都是脏数据。具体执行逻辑如下:
《2.3》以上两种方式的对比:
(3)缓存穿透:
《1》造成原因:
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生 效,这些请求都会打到数据库。
《2》解决方案:
主要解决方案有两种:缓存空对象和布隆过滤器。
《2.1》缓存空对象:
上面我们也说过了造成缓存穿透的主要原因就是用户访问的数据在redis缓存和数据库 中都不存在,造成redis缓存无法起作用。那么我们可以将用户访问的即使在数据库中不存 在的数据也保存至redis缓存中,用户访问时直接可以从redis缓存中获取到数据,这样请求 也 就不会直接穿透缓存到达数据库了。
《2.2》布隆过滤器:
布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希 思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个 请求会去访问 redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数 据库中查询出来这个数据后,再将其放入到redis中。假设布隆过滤器判断这个数据不存在, 则直接返回。
这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思 想,只要哈希思想,就可能存在哈希冲突。
《2.3》两种解决方案的对比:
1.图示:
2.优缺点:
缓存空对象:
优点:实现简单,维护方便
缺点:额外的内存消耗;可能造成短期的不一致
布隆过滤
优点:内存占用较少,没有多余key
缺点:实现复杂 ;存在误判可能
(4)总结:
造成以上三种情景的共同直接原因就是用户的高并发请求在redis缓存中得不到需要的数据,进而这些请求到达数据库,而数据库本身又不擅长处理高并发的请求,因而造成以上三种问题导致数据库崩溃。
正常访问图示:
出现上面问题时的图示:
问题对比:
问题名称 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
---|
资源是否存在DB数据库服务器中 | × | √ | √ |
资源是否存在Redis中 | × | × | × |
redis没有对应资源的原因 | 根本不存在该资源(DB也没有) | 某个热点key过期 | 大部分key集体过期 |
根本原因 | 用户的高并发请求在redis缓存中得不到需要的数据,进而这些请求到达数据库,而数据库本身又不擅长处理高并发的请求,因而造成以上三种问题导致数据库崩溃。 |
解决方案:
问题的主要解决思路如下:
1.让访问请求在缓存中就可以得到需要的数据,不再进一步到数据库访问。比如对缓存 中的数据设置不同的时间避免缓存雪崩。
2.如果需要访问数据库,就让线程一个一个访问,避免高并发问题。比如设置互斥锁或 逻辑过期避免缓存击穿。
3.设置过滤器判断访问的数据是否真实存在,如果不存在则直接返回,不再进行重复性 访问,比如通过布隆过滤避免缓存穿透。
2.内存淘汰机制
(1)概述:
在redis中对于数据的删除策略在大的方向上主要可分为过期删除策略和内存淘汰机制。
过期删除策略主要针对于过期数据,主要的过期删除策略有:定时删除,惰性删除和定时删除。
内存淘汰机制指的是当内存满了之后再存入数据时,redis如何对内存中暂未过期的数据进行选择性删除,主要的内存淘汰机制为逐出算法,也是本篇文章主要探究的重点。
(2)逐出算法:
《1》概述:
Redis使用内存存储数据,在执行每一个命令前,会调用freeMemoryIfNeeded()检测内存 是否充足。如果内存不满足新加入数据的最低存储要求,redis要临时删除一些数据为当前指 令 清理存储空间。清理数据的策略称为逐出算法。
《2》影响数据逐出的相关配置:
《2.1》maxmemory:
最大可使用内存占用物理内存的比例,默认值为0,表示不,生产环境中根据需求设 定,通常设置在50%以上。
《2.2》maxmemory-samples:
每次选取待删除数据的个数。选取数据时并不会全库扫描,导致严重的性能消耗,降 低读写性能。因此采用随机获取数据的方式作为待检测删除数据。
《2.3》maxmemory-policy:
指逐出算法中的删除策略。
《3》逐出算法中的删除策略:
在逐出算法中大的删除策略有两种:执行删除策略和放弃驱逐策略.
《3.1》放弃数据驱逐:
no-enviction(驱逐):
禁止驱逐数据(redis4.0中默认策略),它表示当运行内存超过最 大设置内存时,不淘汰任何数据,这时如果有新的数据写入,则会触发 OOM,但是如 果 没用数据写入的话,只是单纯的查询或者删除操作的话,还是可以正常工作。
《3.2》数据驱逐策略:
检测易失数据(可能会过期的数据集server.db[i].expires ):
① volatile-lru:
挑选最近最少使用的数据淘汰
② volatile-lfu:
挑选最近使用次数最少的数据淘汰
③ volatile-ttl:
挑选将要过期的数据淘汰
④ volatile-random:
任意选择数据淘汰
检测全库数据(所有数据集server.db[i].dict )
⑤ allkeys-lru:
挑选最近最少使用的数据淘汰
⑥ allkeys-lfu:
挑选最近使用次数最少的数据淘汰
⑦ allkeys-random:
任意选择数据淘汰
(3)总结:
《1》LRU算法和LFU算法:
《1.1》LRU算法:
<1>概述:
LRU 全称是 Least Recently Used 翻译为最近最少使用,会选择淘汰最近最少使用的数据。传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。
<2>存在问题:
需要用链表管理所有的缓存数据,这会带来额外的空间开销;
当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
<3>redis中的LRU算法:
Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
LRU 算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。
因此,在 Redis 4.0 之后引入了 LFU 算法来解决这个问题。
《1.2》LFU算法:
<1>概述:
LFU 全称是 Least Frequently Used 翻译为最近最不常用的,LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。
《2》redis中LRU和LFU的实现:
《2.1》LRU算法的实现:
在 LRU 算法中,Redis 对象头的 24 bits 的 lru 字段是用来记录 key 的访问时间戳,因此在 LRU 模式下,Redis可以根据对象头中的 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key。
《2.2》LFU算法的实现:
在 LFU 算法中,Redis对象头的 24 bits 的 lru 字段被分成两段来存储,高 16bit 存储 ldt(Last Decrement Time),低 8bit 存储 logc(Logistic Counter)。
ldt 是用来记录 key 的访问时间戳;
logc 是用来记录 key 的访问频次,它的值越小表示使用频率越低,越容易淘汰,每个新加入的 key 的logc 初始值为 5。
注意,logc 并不是单纯的访问次数,而是访问频次(访问频率),因为 logc 会随时间推移而衰减的。
在每次 key 被访问时,会先对 logc 做一个衰减操作,衰减的值跟前后访问时间的差距有关系,如果上一次访问的时间与这一次访问的时间差距很大,那么衰减的值就越大,这样实现的 LFU 算法是根据访问频率来淘汰数据的,而不只是访问次数。访问频率需要考虑 key 的访问是多长时间段内发生的。key 的先前访问距离当前时间越长,那么这个 key 的访问频率相应地也就会降低,这样被淘汰的概率也会更大。
对 logc 做完衰减操作后,就开始对 logc 进行增加操作,增加操作并不是单纯的 + 1,而是根据概率增加,如果 logc 越大的 key,它的 logc 就越难再增加。
所以,Redis 在访问 key 时,对于 logc 是这样变化的:
先按照上次访问距离当前的时长,来对 logc 进行衰减;
然后,再按照一定概率增加 logc 的值
《3》逐出策略总结: