Redis 过期删除与内存淘汰策略总结
一、Key 过期与删除策略
1.1 如何为 Key 设置过期时间
常用的几种方式(以 key 为例):
- 相对时间:
expire key n:n秒后过期pexpire key n:n毫秒后过期
- 绝对时间戳:
expireat key ts:在秒级时间戳ts之后过期pexpireat key ts:在毫秒级时间戳ts之后过期
- 写入时直接设置过期:
set key value ex n:秒级过期set key value px n:毫秒级过期setex key n value:等价于set + ex
辅助命令:
- 查看剩余存活时间:
ttl key - 取消过期(改为永久):
persist key
1.2 Redis 如何判断 Key 是否过期
Redis 为每个数据库维护两个字典:
dict:真正存放键值对expires:存放键的过期时间(过期字典)
在 expires 中:
- key:指向键对象的指针
- value:long long 类型时间戳,表示过期时刻
当访问某个 key 时,大致流程是:
- 先看该 key 是否出现在
expires中:- 不在:说明没设置过期,直接返回数据
- 在:取出它的过期时间,与当前系统时间对比
- 如果当前时间已超过过期时间,则判定为过期
只判断是否过期还不够,还要解决“何时真正删除”这个问题。
1.3 三种经典过期删除策略
常见的三种思路:
1)定时删除
- 设置过期时间的同时,注册一个定时任务,时间一到立刻删除 key。
- 优点:过期键能尽快释放内存,对内存非常友好。
- 缺点:如果过期键很多,会频繁触发删除任务,占用大量 CPU,影响性能。
2)惰性删除
- 不主动删除;只有当客户端访问某个 key 时,才检查是否过期,若已过期就顺便删除。
- 优点:只在访问时做检查,对 CPU 最友好。
- 缺点:若很久没人访问某些已过期 key,它们会一直占用内存,浪费内存。
3)定期删除
- 定时抽样检查部分 key,发现过期就删除。
- 优点:在 CPU 消耗和内存占用之间做折中。
- 难点:抽样频率、时长不好调:
- 太勤快→类似定时删除,耗 CPU
- 太偷懒→类似纯惰性删除,浪费内存
1.4 Redis 实际采用的策略:惰性删除 + 定期删除
Redis 没有单独使用某一种,而是组合使用:
惰性删除:在访问/修改 key 时调用 expireIfNeeded:惰性删除:在访问/修改 key 时调用 expireIfNeeded
- 未过期:正常返回
- 已过期:删除 key
- 支持同步删除或异步删除,可通过
lazyfree-lazy-expire配置
- 支持同步删除或异步删除,可通过
定期删除:后台定时任务 activeExpireCycle:
- 默认每秒执行若干轮(
hz配置,默认 10) - 每轮从
expires中随机抽取固定数量(默认 20)key:- 检查是否过期,过期就删除
- 若本轮过期比例 > 25%(例如 20 个中超过 5 个过期),继续抽样下一轮
- 整体删除过程有时间上限(默认不超过约 25ms),防止阻塞太久
总结:
- 惰性删除保证“访问时不过期”
- 定期删除保证“就算没人访问,过期数据也会逐步被清理”
- 两者结合,在 CPU 与内存之间达成平衡
二、内存淘汰策略(maxmemory 触发后的行为)
过期删除解决的是“已经过期的数据”,但如果大量永久 key 或过期时间很长的 key 塞满了内存怎么办?这时轮到“内存淘汰策略”出场。
2.1 最大内存 maxmemory 设置
在 redis.conf 中:
maxmemory <bytes>:设置最大可用内存- 64 位默认值为
0:不限制,只要系统内存够用 - 32 位默认约
3GB:防止超过进程可用空间而崩溃
- 64 位默认值为
只有当实际使用内存超过 maxmemory 时,才会触发内存淘汰逻辑。
2.2 内存淘汰触发流程
大致步骤:
- 客户端发起写入命令(如
set、hset等) - Redis 检查当前内存是否超过
maxmemory - 如果超过,则根据配置的淘汰策略,挑选一批 key 删除
- 删除后继续尝试写入:
- 能腾出足够空间:写入成功,并可触发
evicted等通知事件 - 腾不出空间且策略为
noeviction:写入失败,返回 OOM 错误
- 能腾出足够空间:写入成功,并可触发
2.3 八种内存淘汰策略概览
从是否“只考虑设置了 TTL 的 key”来划分:
- 只在有 TTL 的 key 范围内淘汰(
volatile-*) - 在所有 key 范围内淘汰(
allkeys-*) - 完全不淘汰(
noeviction)
具体 8 种策略如下。
2.3.1 仅对有 TTL 的 key 淘汰(4 种)
volatile-lru- 在“设置了过期时间的 key”中,按照最近最少使用(LRU)原则淘汰
volatile-lfu- 在“设置了过期时间的 key”中,按访问频率最低(LFU)淘汰
volatile-random- 在“设置了过期时间的 key”中随机淘汰
volatile-ttl- 在“设置了过期时间的 key”中,优先淘汰剩余寿命最短的 key
2.3.2 对所有 key 淘汰(3 种)
allkeys-lru- 在所有 key 中按 LRU 淘汰
allkeys-lfu- 在所有 key 中按 LFU 淘汰
allkeys-random- 在所有 key 中随机淘汰
2.3.3 不淘汰(1 种)
noeviction- 不删除任何 key
- 当内存不足时,新的写入直接返回错误(OOM),读和删仍然正常
2.4 策略选择建议
根据业务特点选择:
- 典型缓存场景(数据都可被淘汰):
- 推荐:
allkeys-lru或allkeys-lfu - 避免写满内存导致实例不可用
- 推荐:
- 同时存在“长期保存数据 + 缓存数据”:
- 一部分 key 没有 TTL,需要尽量保留
- 推荐:
volatile-lru或volatile-lfu - 只在设置了过期时间的 key 中做淘汰,尽量不动“永久数据”
- 极端严格的数据保护:
- 推荐:
noeviction - 前提是:做好监控和扩容,不然写满后会频繁报错
- 推荐:
2.5 查看和修改当前策略
- 查看当前策略:
config get maxmemory-policy
- 临时修改(重启后失效):
config set maxmemory-policy allkeys-lru
- 永久修改:在
redis.conf中设置:
maxmemory-policy allkeys-lru
三、LRU 与 LFU:原理与 Redis 实现
Redis 4.0 之后引入 LFU,是为了弥补 LRU 在某些场景下的不足(例如缓存污染)。
3.1 LRU:最近最少使用
概念:淘汰近期最少被访问的数据,假设“最近用过的将来还会用”。
传统实现:
- 使用双向链表维护所有缓存对象:
- 新访问/更新的元素移动到链表头
- 淘汰时从链表尾部删除
- 问题:
- 需要为所有对象维护链表指针,增加内存开销
- 每次访问都要移动节点,访问量大时会产生较高的 CPU 开销
3.2 Redis 的近似 LRU 实现
Redis 为了在“效果”和“开销”之间折中:
- 在对象头中增加一个 24 位字段
lru,记录“最后一次访问时间” - 当需要淘汰时,不是遍历所有 key,而是:
- 随机抽样固定数量键(默认 5,可配置)
- 在这批样本中找到最久没访问的那个淘汰
- 优点:
- 不需要维护全局链表,节省内存
- 访问时只更新字段,不涉及链表移动,开销小
- 缺点:
- 不是完全精确的 LRU,而是“近似 LRU”,但对大多数缓存场景足够好
3.3 LRU 的问题:缓存污染
典型问题:
- 某一次业务批量扫描大量冷数据(只访问一次)
- 在 LRU 策略下,这些“一次性访问”的数据会被认为是“最近访问过”,反而把真正热点数据挤出缓存
- 访问结束后,这些冷数据会在缓存中停留很久,造成缓存污染
为解决这个问题,引入 LFU。
3.4 LFU:最近最不常用
核心思路:既看“是否访问过”,更看“访问频率”。
- 假设:过去访问频繁的 key,未来也更可能被访问
- 淘汰:优先淘汰“访问频率最低”的 key,而不是“最久没访问”的 key
3.5 Redis 中的 LFU 实现细节
在 LFU 模式下,Redis 仍然使用对象头中的 24 位 lru 字段,但进行了拆分:
- 高 16 位:
ldt(Last Decrement Time,记录上次频率衰减的时间) - 低 8 位:
logc(Logistic Counter,访问频率计数)
含义:
logc越小,表示该 key 使用频率越低,被淘汰概率越大- 新建 key 的
logc一般从较小值起步(例如 5)
访问 key 时主要做两件事:
- 频率衰减(decay)
- 根据当前时间与
ldt的差值,决定衰减多少 - 时间间隔越大,衰减越多
- 目的:让“很久以前曾经热过”的 key 随时间逐渐不再被视为热点
- 根据当前时间与
- 频率增加(increment)
- 不是简单
logc++,而是按概率增加:logc越大,想再变得更大就越难- 防止单个极热 key 在长时间内把计数冲得太高,影响整体效果
- 不是简单
相关配置项(在 redis.conf 中):
lfu-decay-time(默认 1 分钟)- 控制衰减速度,值越大衰减越慢
lfu-log-factor- 控制频率增长速度,值越大,想让
logc再上涨就更难
- 控制频率增长速度,值越大,想让
通过调整这两个参数,可以微调 LFU 的“记性”和“遗忘速度”。
四、小结
- 过期删除:
- Redis 使用“惰性删除 + 定期删除”的组合:
- 惰性删除:访问时顺手检查并清理
- 定期删除:后台定时抽样清理
- 目标:在 CPU 开销和内存占用之间取得平衡
- Redis 使用“惰性删除 + 定期删除”的组合:
- 内存淘汰:
- 当实际内存超过
maxmemory时触发 - 共有 8 种策略:
volatile-*:只在有 TTL 的 key 中淘汰allkeys-*:在所有 key 中淘汰noeviction:不淘汰,只报错
- 缓存场景优先推荐
allkeys-lru/allkeys-lfu
- 当实际内存超过
- LRU vs LFU:
- LRU:根据“最近是否访问”淘汰,简单高效,但容易缓存污染
- LFU:根据“访问频率”淘汰,更能体现真正热点
- Redis 实现的都是“近似版本”,通过采样和紧凑计数字段在效果与性能之间做了折中