本文,你将阅读到以下内容:
- 如何应对缓存击穿和缓存雪崩的问题;
- Redis 的过期策略以及内存淘汰机制;
1.如何应对缓存击穿和缓存雪崩的问题?
场景:在大并发情况下,流量几百万左右;
-
概念:
- 缓存穿透:故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,然而数据库中也不存在,直接返回null,要是有人利用不存在的key频繁攻击我们的应用,那就有可能造成数据库的崩溃;
- 缓存雪崩:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩;
- 缓存击穿:缓存中的某一个key在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮;
-
解决:
- 缓存穿透 or 缓存击穿解决方案
- 布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力;
- 缓存null值:如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
- 缓存雪崩解决方案:
- 缓存失效时间分散开来:可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件;
- 使用互斥锁(该方案吞吐量明显会下降):用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上;
- 双缓存:设计两个缓存,缓存A和缓存B,缓存A的设置过期时间,缓存B不设失效时间,自己做缓存预热操作;(可以细分下设计:从缓存A 读数据,有则直接返回;A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程,更新线程同时更新缓存A和缓存B);
- 缓存穿透 or 缓存击穿解决方案
2.Redis 的过期策略以及内存淘汰机制
场景:
比如你的Redis只能存5G数据,可是你写了10G,那会删5G的数据,怎么删的? 这个问题思考过么?
或者,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?-
概念
- 被动删除:用到的时候才会去检验key是不是已过期,过期就删除;
- 主动删除:Redis会定期主动淘汰一批已过去的key;
-
原因:
- 定时删除(主动删除)
- 含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除;
- 优点:保证内存被尽快释放;
- 缺点:若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生)性能影响严重没人用;
- 定期删除(主动删除)
- 含义:每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key操作
- 优点:通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点定期删除过期key--处理"惰性删除"的缺点.
- 缺点:在内存友好方面,不如"定时删除"在CPU时间友好方面,不如"惰性删除"
- 难点:合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)这个要根据服务器运行情况来定了.
- 惰性删除(被动删除)
- 含义:key过期的时候不删除,每次从缓存中获取key的时候去检查是否过期,若过期,则删除,返回null。
- 优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)
- 缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存);
- 定时删除(主动删除)
-
过期策略配置
- redis 内部默认采用的是定期删除+惰性删除策略;
- 定期删除+惰性删除流程:
- 惰性删除流程:在进行get或setnx等操作时,先检查key是否过期,若过期,删除key,然后执行相应操作;若没过期,直接执行相应操作;
- 定期删除流程:遍历每个数据库(就是redis.conf中配置的"database"数量,默认为16),循环遍历当前库中的指定个数个key(默认是每个库检查20个key),如果当前库中没有一个key设置了过期时间,直接执行下一个库的遍历,随机获取一个设置了过期时间的key,检查该key是否过期,如果过期,删除key.判断定期删除操作是否已经达到指定时长(配置redis.conf 的hz选项),若已经达到,直接退出定期删除. 也可以通过配置redis.conf的maxmemory最大值,当已用内存超过maxmemory限定时,就会触发定期删除策略;
- 定期删除+惰性删除流程:
- 过期key对持久化RDB和AOF的影响
- 从内存数据库持久化数据到RDB文件,持久化key之前,会检查是否过期,过期的key不进入RDB文件;
- 从RDB文件恢复数据到内存数据库,在数据载入内存数据库之前,会对key先进行过期检查,如果过期,不导入数据库.
- 从内存数据库持久化数据到AOF文件,当key过期后,还没有被删除时,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令).
- 当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉);
- AOF重写时,会先判断key是否过期,已过期的key不会重写到aof文件.
- 采用定期删除+惰性删除就没其他问题了么?
- 并不是这样的,如果定期删除没删除Key,然后你也没即时去请求Key,也就是说惰性删除也没生效.这样,Redis的内存会越来越高.那么就应该采用内存淘汰机制.
- redis 内部默认采用的是定期删除+惰性删除策略;
-
内存淘汰机制
- 原因:Redis内存淘汰指的是用户存储的一些键被可以被它主动地从实例中删除,从而产生读miss的情况,内存的淘汰机制的初衷是为了更好地使用内存,用一定的缓存miss来换取内存的使用效率;
- 配置:
- Redis内存淘汰通过配置redis.conf中的maxmemory这个值来开启,maxmemory为0的时候表示我们对Redis的内存使用没有限制;
- 淘汰策略的选择可以通过redis.conf中的maxmemory-policy来配置;
- 主键空间 && 设置了过期时间的键空间:在redis中,如果有一批键存储在Redis中,则有那么一个哈希表用于存储这批键及其值,如果这批键中有一部分设置了过期时间,那么这批键还会被存储到另外一个哈希表中.设置了过期时间的键空间为主键空间的子集。
- 淘汰策略:
- noeviction:当redis内存数据达到maxmemory,在该策略下,新写入数据时直接返回OOM错误;
- allkeys-lru:当内存不足以容纳新写入数据时,在主键空间中,优先移除最近未使用的一些key[以配置文件中的maxmemory-samples个key作为样本池进行抽样清理];
- allkeys-random:当内存不足以容纳新写入数据时,在主键空间中,随机移除某些key[以配置文件中的maxmemory-samples个key作为样本池进行抽样清理];
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key[以配置文件中的maxmemory-samples个key作为样本池进行抽样清理],这种情况一般是把redis既当缓存,又做持久化存储的时候才用.
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某些key.
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key[以配置文件中的maxmemory-samples个key作为样本池进行抽样清理]优先移除;
- 注意:
- 淘汰策略的清理过程是阻塞的,直到清理出足够的内存空间.所以如果在达到maxmemory并且调用方还在不断写入的情况下,可能会反复触发主动清理策略,导致请求会有一定的延迟.
- 尽量不要触发maxmemory,最好在mem_used内存占用达到maxmemory的一定比例后,需要考虑调大hz以加快淘汰,或者进行集群扩容.
- 如果能够控制住内存,则可以不用修改maxmemory-samples配置;如果Redis本身就作为LRU cache服务(这种服务一般长时间处于maxmemory状态,由Redis自动做LRU淘汰),可以适当调大maxmemory-samples.