Redis的零食盒满了怎么办?详解缓存淘汰策略

Redis 缓存过期淘汰策略

第一站:为什么要"淘汰"?(概念铺垫)

首先,我们得明白为什么要费劲心思地设计这些"淘汰策略"?

通俗类比:

想象你的 Redis 就像一个 容量有限的"零食盒"

  1. 过期时间(TTL): 你买了一堆零食,有些零食(数据)是有保质期(TTL,Time To Live,存活时间)的。时间一到,即使盒子没满,这个零食也得扔掉,因为它"过期"了。
  2. 内存限制(Maxmemory): 你的零食盒就这么大(maxmemory 配置),当盒子满了,你又想放新的零食进去,怎么办?你必须先扔掉一些旧的零食腾出空间。

过期策略 解决的是 "时间一到就扔" 的问题。

淘汰策略 解决的是 "盒子满了该扔谁" 的问题。

我们本次主要关注的是第二个问题,即 maxmemory 限制下的 淘汰策略
否 是 Redis内存使用 内存是否达到maxmemory? 正常写入数据 触发淘汰策略 根据策略选择淘汰键 删除选中键释放内存 写入新数据


第二站:Redis 的两大基石:过期和淘汰

在介绍具体的淘汰策略前,我们先快速了解一下 Redis 如何处理"过期"这件事,因为它和"淘汰"策略是并存的。

数据的"过期"机制(Expiration)

Redis 处理过期数据的机制,被称为 惰性删除(Lazy Deletion)定期删除(Active Deletion)

A. 惰性删除(Lazy Deletion):随用随清
  • 学术解释: 当客户端尝试访问(GET/MGET 等操作)某个带有 TTL 的键时,Redis 会先检查这个键的过期时间,如果发现它已经过期,则在返回结果前将其删除。
  • 通俗比方: 就像你打开零食盒,拿起一块饼干时,先闻一闻、看一看日期,发现过期了,马上扔掉,而不是放回盒子里。
  • 优点: 节省 CPU 资源,只有被访问时才检查,避免了不必要的扫描。
  • 缺点: 如果大量数据过期后一直没被访问,它们会一直占用内存,直到被淘汰策略处理。
B. 定期删除(Active Deletion):抽样巡检
  • 学术解释: Redis 会周期性地(默认每秒执行 10 次)随机抽取一些设置了 TTL 的键进行检查,并删除其中已过期的键。
    • 这个过程是有限制的,例如每次执行时长不超过 25 毫秒。
  • 通俗比方: 零食盒旁边有个"巡检员",每隔一段时间(100 毫秒)就随便拿出几包零食看看有没有过期,发现过期就扔掉。
  • 优点: 弥补了惰性删除的不足,可以清理一些不常访问但已过期的数据。

Client Redis 过期键 惰性删除流程 GET key1 检查key1是否过期 删除key1 返回nil 返回value alt [已过期] [未过期] 定期删除流程 随机抽取20个带TTL的键 检查每个键是否过期 删除过期键 loop [对每个过期键] 执行时间≤25ms loop [每秒10次] Client Redis 过期键


第三站:缓存淘汰策略(Eviction Policies)

Redis 内存达到 maxmemory 限制,并且有新的数据需要写入时,淘汰策略 就会启动,来决定"牺牲"哪些数据。

Redis 提供了 8 种主要的淘汰策略(自 Redis 4.0 以后):

A. 不淘汰策略(No Eviction)

  • 策略名: noeviction
  • 作用: 当内存不足,且有新的写入命令时,直接返回错误,不会删除任何键。
  • 底层哲学: "宁可报错,不删数据"。适用于那些对数据完整性要求极高,或依赖外部机制保证内存管理的应用。
  • 比方: 零食盒满了,你想放新零食进来,但守卫说:"没地方了,你得等别人吃了或扔了腾出位置再说,我现在不会替你扔任何东西。"

B. 针对"设置了过期时间"的键进行淘汰(Volatile Family)

这组策略只关注那些设置了 TTL 的键。

  1. volatile-lru (Least Recently Used)
    • 作用: 从所有设置了过期时间的键中,淘汰 最近最少使用 的键。
    • 核心思想: 那些长时间没人碰的,以后用到的概率也小,优先淘汰它们。
  2. volatile-lfu (Least Frequently Used)
    • 作用: 从所有设置了过期时间的键中,淘汰 最不经常使用 的键。
    • 核心思想: 相比 LRU 关注"最后一次使用时间",LFU 更关注"使用次数"。使用次数少的,优先淘汰。
  3. volatile-ttl (Time To Live)
    • 作用: 从所有设置了过期时间的键中,淘汰 剩余 TTL 值最小 的键(即最快要过期的键)。
    • 核心思想: 反正快过期了,不如先扔掉,让过期机制的工作更简单。
  4. volatile-random
    • 作用: 从所有设置了过期时间的键中,随机 淘汰键。
    • 核心思想: 简单粗暴,不求性能最优,只求执行速度快。

C. 针对"所有键"进行淘汰(Allkeys Family)

这组策略会考虑 Redis 数据库中的 所有键,无论是否设置了 TTL。

  1. allkeys-lru (Least Recently Used)
    • 作用:所有键 中,淘汰 最近最少使用 的键。
    • 核心思想: 相比 volatile-lru,它连永久键(没有设置 TTL 的键)也敢删。
  2. allkeys-lfu (Least Frequently Used)
    • 作用:所有键 中,淘汰 最不经常使用 的键。
  3. allkeys-random
    • 作用:所有键 中,随机 淘汰键。

Redis淘汰策略 noeviction
不淘汰 volatile家族
只淘汰有TTL的键 allkeys家族
淘汰所有键 volatile-lru
最近最少使用 volatile-lfu
最不经常使用 volatile-ttl
最快过期 volatile-random
随机淘汰 allkeys-lru
最近最少使用 allkeys-lfu
最不经常使用 allkeys-random
随机淘汰


第四站:底层解剖:LRU 和 LFU 的"近似"实现

LRU 和 LFU 是最常用的淘汰策略,但 Redis 的实现并非是 完全精准 的,而是 近似(Approximation) 的。

1. 为什么是"近似"?

  • 学术解释: 完全精准 的 LRU/LFU 需要维护一个全局有序链表(LRU)或复杂的频率计数结构(LFU)。对于拥有数百万键的 Redis 来说,每次访问或插入/删除都需要移动或更新这些结构,这将带来巨大的 性能开销,完全无法达到 Redis 所追求的高并发、低延迟目标。
  • 通俗比方: 想象你有一千万本图书,要找出"最近最少被借阅"的那一本。如果每借出一本,你都要移动或排序一千万本图书的记录,那图书馆就瘫痪了。

2. Redis 的"近似 LRU" (allkeys-lru / volatile-lru)

Redis 采用的是 随机采样法 来近似实现 LRU。

  • 实现原理:
    1. Redis 维护了一个 24-bit 的字段(lru 字段 )记录每个键的 最后一次被访问的时间戳(相对时间)。
    2. 当需要淘汰时,随机选择 少量键(例如,默认配置下选择 maxmemory-samples 个键,默认为 5 个)。
    3. 从这 5 个随机选出的键 中,淘汰 掉那个 lru 字段最小(即最后一次访问时间最久)的键。
  • 比方: 守卫不是挨个检查所有零食,而是 随机抓出 5 个 ,比较它们的生产日期(最后访问时间),扔掉 这 5 个里面最久远的那个。
  • 总结: 随机采样、局部最优,效率极高,效果接近真正的 LRU。

内存达到maxmemory 随机选择5个键作为候选池 遍历候选池查找LRU字段最小的键 淘汰找到的LRU最小键 释放内存空间 键被访问时 更新该键的LRU字段

3. Redis 的"近似 LFU" (allkeys-lfu / volatile-lfu)

LFU 比 LRU 复杂,它关注的是使用 频率

  • 实现原理:
    1. Redis 使用了一个 24-bit 的字段(lfu 字段 )记录每个键的 访问频率 。这个字段被分为两部分:
      • 高 8 位记录 访问频率计数器(counter
      • 低 16 位记录 访问时间戳(ltime ,用于对频率进行 衰减
    2. 计数器增长: 每次键被访问,counter 都会递增,但不是简单地 +1,而是使用 概率对数计数 的方式,保证高频率的键不会无限增长,而是缓慢趋近一个上限。
    3. 频率衰减: 如果一个键长时间(例如,几分钟)没有被访问,counter 会根据 ltime 的信息自动 衰减,防止长时间不访问的"历史高频键"霸占内存。
  • 比方: 守卫给每包零食贴一个"热度标签",每次有人拿它,热度就增加一点。但如果长时间没人碰,热度会随着时间自动冷却。淘汰时,就找那些 热度最低 的扔掉。
  • 总结: LFU 通过频率衰减机制,更好地适应了"热点漂移"的情况,即一个键曾经很热,但现在不再使用了,它最终会被淘汰。

初始状态 频繁访问 持续访问 访问减少 长时间未访问 被淘汰 低频键 中频键 高频键 counter值高
但会随时间衰减


第五站:Java 后端技术栈的选择建议

作为 Java 后端开发者,理解这些策略后,如何选择呢?

策略 适用场景 优点 缺点 推荐指数
allkeys-lru 最常用和推荐的。当你不知道选择什么时,选它。适用于大多数业务场景,如 Session 缓存、热门商品列表等。 效果好,命中率高,性能高(近似实现)。 可能淘汰掉永不过期但很久没用的关键配置数据。 ⭐⭐⭐⭐⭐
volatile-lru 当你需要将 重要配置或字典数据 设置为永不(或极长 TTL)过期,不希望被淘汰,但又需要缓存大量临时数据时。 兼顾了重要数据的保护和缓存淘汰的需求。 只能淘汰设置了 TTL 的键。 ⭐⭐⭐⭐
allkeys-lfu 适用于 热点数据分布非常不均匀 ,且希望长期不被访问的键能比 LRU 更长时间保留 的场景。 对高频键的保护比 LRU 更好。 略微复杂,计算开销略高于 LRU。 ⭐⭐⭐
noeviction 仅用于 非缓存应用(如作为分布式锁服务)或你完全掌控内存容量,绝不允许数据丢失的场景。 保证数据完整性。 内存一满立即无法写入。 ⭐⭐

40% 25% 20% 10% 5% Redis淘汰策略使用推荐 allkeys-lru (首选推荐) volatile-lru (配置保护) allkeys-lfu (热点场景) noeviction (特殊场景) 其他策略

总结:

在绝大多数 Java 缓存场景中,allkeys-lru 是首选。它简单、高效,并且能很好地模拟出热点数据的特性。如果你的 Redis 既做缓存又做配置存储,可以考虑 volatile-lru,将配置数据不设置 TTL 来保护起来。

相关推荐
惜.己2 小时前
jmeter中java.net.ConnectException: Connection refused: connect
java·jmeter·.net
yunmi_3 小时前
分布式文件存储系统FastDFS(入门)
java·分布式·maven·fastdfs
K_i1344 小时前
指针步长:C/C++内存操控的核心法则
java·开发语言
宇宙超粒终端控制中心4 小时前
Java使用easypoi填充数据到word
java·spring boot·spring cloud·java-ee·easypoi
又是忙碌的一天5 小时前
java学习:四大排序
java·学习·排序算法
城管不管5 小时前
面试题(1)
java
二饭5 小时前
POI操作Docx的踩坑指南(一)
java·apache
李贺梖梖6 小时前
DAY25 综合案例
java
-雷阵雨-6 小时前
数据结构——优先级队列(堆)
java·开发语言·数据结构·intellij-idea