【Redis】缓存:过期、淘汰策略、三大问题
缓存
过期删除策略
Why?
对 key 设置过期时间,需要有相应的机制将已过期的键值对删除
How?
-
过期字典:保存所有 key 的过期时间。
-
typedef struct redisDb { //数据库键空间,存放着所有的键值对 dict *dict; //键的过期时间 dict *expires; .... } redisDb;
-
检查该 key 是否存在于过期字典中:
- 不在,正常读取键值;
- 在,获取该 key 的过期时间,与当时间比对,判定是否过期。
常见的三种过期删除策略
-
定时删除:当时间到达时,由事件处理器自动执行 key 的删除操作。
- 删除非常快
- 对CPU不友好
-
惰性删除:使用时发现过期才删除
- 对CPU友好、内存空间浪费
-
定期删除:每隔一段时间「随机」取出一定数量的 key 进行检查和删除
-
限制删除执行的时长和频率,减少CPU占用、内存也不会浪费太多
-
难以确定删除操作执行的时长和频率。
-
Redis 过期删除策略
惰性删除+定期删除:合理使用 CPU 时间和避免内存浪费之间取得平衡。
-
惰性删除:访问或者修改 key 前,调用 expireIfNeeded 函数检查 key 是否过期:
- 过期:删除该 key,选择异步删除,还是同步删除,根据
lazyfree_lazy_expire
参数配置决定(Redis 4.0版本开始提供参数),然后返回 null 客户端; - 没有过期:不做任何处理,返回正常的键值
- 过期:删除该 key,选择异步删除,还是同步删除,根据
-
定期删除:
-
hz 10:10s一次
-
从过期字典中随机抽取 20 个 key;
-
检查是否过期并删除已过期的 key;
-
本轮已过期超过 5 个(25%),继续重复步骤 1;小于 25%,则停止。
-
定期删除循环流程的时间上限25ms
-
内存淘汰策略
Why?
Redis 的运行内存超过最大内存后,使用内存淘汰策略删除符合条件的 key,以此来保障 Redis 高效的运行。
- maxmemory :设置最大内存,默认0无限制
How?
Redis 内存淘汰策略
-
不淘汰
- noeviction(Redis3.0默认) :超过最大内存,不淘汰任何数据,有新的数据写入 OOM,查询删除正常。
-
数据淘汰
-
设置过期时间的数据中进行淘汰:
- volatile-random:随机淘汰设置了过期时间的任意键值;
- volatile-ttl:优先淘汰更早过期的键值。
- volatile-lru(Redis3.0之前默认):淘汰所有设置了过期时间的键值中,最久未使用的键值;
- volatile-lfu(Redis 4.0 新增):淘汰所有设置了过期时间的键值中,最少使用的键值;
-
在所有数据范围内进行淘汰:
- allkeys-random:随机淘汰任意键值;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- allkeys-lfu(Redis 4.0新增):淘汰整个键值中最少使用的键值。
-
查看当前 Redis 使用的内存淘汰策略:config get maxmemory-policy
修改内存淘汰策略:
config set maxmemory-policy <策略>
:立即生效,重启恢复默认- 修改配置文件
maxmemory-policy <策略>
:重启生效
LRU 算法和 LFU 算法有什么区别?
-
LRU(Least Recently Used):最近最少使用,最近最久未使用
-
传统实现:链表
- 额外空间开销、移动链表耗时
-
Redis实现:近似 LRU 算法(节约内存、CPU)
- Redi对象结构体中添加字段,记录此数据的最后一次访问时间。
- 随机取 5 个(可配置)值,淘汰最久没有使用的那个。
- 缺点:无法解决缓存污染问题
-
-
LFU(Least Frequently Used):最近最不常用,根据数据访问次数来淘汰
-
Redis实现:redisObject 记录「数据的访问频次」
- ldt:访问时间戳
- logc:访问频次(频率)
-
先按照上次访问距离当前时长,对 logc 衰减;
-
再按照一定概率增加 logc 的值
-
typedef struct redisObject {
...
// 24 bits
//用于记录对象的访问信息
unsigned lru:24;
...
} robj;
- LRU:记录 key 的访问时间戳
- LFU:高 16bit ldt(Last Decrement Time),低 8bit logc(Logistic Counter)。
- lfu-decay-time:调整 logc 的衰减速度,分钟为单位,默认值为1,越大越慢;
- lfu-log-factor :调整 logc 的增长速度,值越大,增长越慢。
缓存雪崩
Why?
Redis大量数据同时过期、Redis故障,导致服务器对数据库发起大量请求,击垮数据库
How?
解决办法
针对大量数据同时过期:
-
均匀设置过期时间:设置时间时加上随机数
-
互斥锁:服务数据库前加锁,保证只有一个服务访问数据库
-
双 key 策略(分级缓存):两倍空间浪费
- 主 key,设置过期时间。备 key,不会设置过期。 value 值一样。
- 访问不到主 key,返回备key并更新主备的数据
-
后台更新缓存:缓存永久有效,后台线程定时更新缓存
-
当系统内存紧张的时候,有些缓存数据会被“淘汰”,到下次后台定时更新前无法访问
- 后台线程不仅定时更新缓存,也频繁地(ms级)检测缓存是否有效
-
发现缓存数据失效后(缓存数据被淘汰),通过消息队列通知后台线程更新缓存
- 后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。更新会更及时。
-
针对Redis故障:
-
服务熔断或请求限流机制;
- 服务熔断机制:暂停业务应用对缓存服务的访问,直接返回错误,不用继续访问数据库
- 请求限流机制:只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。
-
构建 Redis 缓存高可靠集群;
-
主从节点的方式构建 Redis 缓存高可靠集群
-
缓存击穿
Why?
热点数据过期,大量的请求访问了该热点数据,直接访问数据库,数据库被高并发的请求冲垮
How?
- 分布式锁+doublecheck:保证同一时间只有一个业务线程更新缓存,未获取互斥锁的请求,等待锁释放后重新读取缓存或返回空值或者默认值。
- 不给热点数据设置过期时间+LFU:由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
缓存穿透
Why?
缓存穿透:大量用户访问的数据,既不在缓存中,也不在数据库中
- 业务误操作:缓存中的数据和数据库中的数据都被误删除了
- 黑客恶意攻击:故意大量访问某些读取不存在数据的业务
How?
-
非法请求的限制;
-
缓存空值或者默认值;
- 针对查询的数据,在缓存中设置一个空值或者默认值,后续请求不会继续查询数据库。
-
使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;
如何设计一个缓存策略,可以动态缓存热点数据呢?
通过数据最新访问时间来做排名,定期过滤掉不常访问的数据,只留下经常访问的数据。
转载自:https://juejin.cn/post/7243987464297070647