Skip to content

Redis 过期删除与内存淘汰原理

Redis 里经常设置 TTL:

bash
set user:1001 value ex 3600

但很多人不知道 TTL 到期后 Redis 到底什么时候删除 key,也分不清“过期删除”和“内存淘汰”。

这一页解决:

  1. TTL 存在哪里。
  2. key 过期后是不是立刻删除。
  3. 惰性删除和定期删除分别做什么。
  4. 内存满了为什么会触发淘汰。
  5. volatile-lruallkeys-lrunoeviction 怎么选。
  6. 为什么过期 key 堆积、淘汰频繁会导致抖动。

过期删除和内存淘汰不是一回事

机制触发条件目的
过期删除key 到达 TTL 时间删除已经过期的数据
内存淘汰Redis 使用内存超过 maxmemory腾出空间给新写入

一个 key 可以:

  1. 到期后被过期删除。
  2. 没到期但因为内存压力被淘汰。
  3. 没设置 TTL,但在 allkeys 策略下也可能被淘汰。

TTL 存在哪里

Redis 主字典保存 key 到 value。

过期字典保存 key 到过期时间。

mermaid
flowchart TD
    A["主字典 dict"] --> B["user:1001 -> value"]
    C["过期字典 expires"] --> D["user:1001 -> expire timestamp"]
    B --> E["真正的数据"]
    D --> F["什么时候过期"]

设置 TTL 时,不是把过期时间塞进 value,而是在过期字典里记录过期时间。

key 到期会立刻删除吗

不会保证立刻删除。

如果所有 key 到期瞬间都立刻删除,Redis 要维护大量定时器,成本很高。

Redis 常用组合:

  1. 惰性删除。
  2. 定期删除。

惰性删除

访问 key 时检查它是否过期。

mermaid
flowchart TD
    A["客户端访问 key"] --> B{"key 有 TTL 吗"}
    B -- "没有" --> C["正常返回"]
    B -- "有" --> D{"当前时间超过过期时间吗"}
    D -- "否" --> C
    D -- "是" --> E["删除 key"]
    E --> F["返回不存在"]

优点:

  1. 简单。
  2. 只处理被访问的 key。
  3. 不浪费太多 CPU。

缺点:

  1. 如果过期 key 再也没人访问,它可能留在内存里。

定期删除

Redis 会周期性抽样检查带过期时间的 key,删除已经过期的。

mermaid
flowchart TD
    A["周期任务触发"] --> B["从 expires 字典抽样"]
    B --> C["检查是否过期"]
    C --> D["删除过期 key"]
    D --> E{"过期比例是否较高"}
    E -- "是" --> F["继续多做一些清理"]
    E -- "否" --> G["结束本轮,避免占用太多 CPU"]

为什么是抽样,不是全量扫描?

全量扫描过期字典会阻塞主线程,key 多时风险很大。

为什么过期 key 会堆积

可能原因:

  1. 大量 key 同时设置相同 TTL。
  2. key 到期后不再被访问,惰性删除触发少。
  3. 定期删除抽样清理不过来。
  4. Redis CPU 忙,清理任务压力大。

后果:

  1. 内存看起来降不下来。
  2. 后续访问过期 key 时触发删除,增加延迟。
  3. 内存压力触发淘汰。

内存淘汰什么时候发生

如果配置了:

conf
maxmemory 4gb
maxmemory-policy allkeys-lru

当 Redis 内存超过上限,写入新数据时会尝试淘汰一些 key。

mermaid
flowchart TD
    A["写入新 key"] --> B{"内存超过 maxmemory 吗"}
    B -- "否" --> C["正常写入"]
    B -- "是" --> D["根据淘汰策略选择 key"]
    D --> E["删除被淘汰 key"]
    E --> F{"内存够了吗"}
    F -- "够" --> C
    F -- "不够" --> G["继续淘汰或返回错误"]

常见淘汰策略

策略含义适合
noeviction不淘汰,写入报错不允许自动丢 key
allkeys-lru所有 key 中淘汰最近最少使用纯缓存场景
volatile-lru只从设置 TTL 的 key 中按 LRU 淘汰部分 key 可淘汰
allkeys-lfu所有 key 中淘汰低频访问热点更稳定的缓存
volatile-lfu设置 TTL 的 key 中淘汰低频访问部分 key 可淘汰
allkeys-random所有 key 随机淘汰很少作为精细策略
volatile-ttl淘汰剩余 TTL 较短的 key希望优先淘汰快过期数据

LRU 和 LFU 的差异

LRU 看最近有没有用过,LFU 看访问频率。

mermaid
flowchart TD
    A["淘汰判断"] --> B["LRU: 最近是否访问"]
    A --> C["LFU: 访问次数是否低"]
    B --> D["适合访问最近性明显的场景"]
    C --> E["适合长期热点比较稳定的场景"]

例子:

  1. 新闻热点变化快:LRU 可能更合适。
  2. 全站配置、热门商品长期热:LFU 可能更合适。

为什么 TTL 要加随机值

错误写法:

bash
set product:1 value ex 3600
set product:2 value ex 3600
set product:3 value ex 3600

如果大量 key 同一时间写入,又同一时间过期,会出现缓存雪崩。

更推荐:

text
TTL = 基础时间 + 随机偏移

例如:

java
int ttl = 3600 + ThreadLocalRandom.current().nextInt(300);

商业场景:商品详情缓存

商品详情:

bash
set product:detail:1001 json ex 3900

推荐策略:

  1. 设置 TTL,避免旧缓存永久存在。
  2. TTL 加随机值,避免同时过期。
  3. 热点商品用逻辑过期或预热。
  4. 大 JSON 控制大小,避免大 Key。
  5. Redis 异常时限流保护数据库。

商业场景:验证码缓存

验证码适合短 TTL:

bash
set sms:code:13800138000 123456 ex 300

淘汰策略要注意:如果验证码还没过期却被内存淘汰,用户会校验失败。所以验证码这类临时但业务敏感数据,需要监控内存,避免随便被淘汰。

内存高怎么排查

bash
info memory
info stats
memory usage key
redis-cli --bigkeys
scan 0 count 100

排查顺序:

mermaid
flowchart TD
    A["Redis 内存高"] --> B["看 maxmemory 和 used_memory"]
    B --> C["查是否有大 Key"]
    C --> D["查是否大量无 TTL Key"]
    D --> E["查过期 key 是否清理慢"]
    E --> F["看 evicted_keys 是否增长"]
    F --> G["评估淘汰策略和业务模型"]

关注指标:

指标含义
used_memoryRedis 分配的内存
used_memory_rss操作系统看到的常驻内存
expired_keys过期删除 key 数量
evicted_keys淘汰 key 数量
mem_fragmentation_ratio内存碎片比例

常见坑

后果正确做法
所有 key 同一 TTL同时过期引发雪崩TTL 加随机
纯缓存用 noeviction内存满后写入报错纯缓存可考虑 allkeys-lru/lfu
关键临时数据随意淘汰验证码、锁等提前丢失单独实例或谨慎策略
不设置 TTL内存持续增长缓存必须有生命周期
大 Key 删除用 DEL阻塞 Redis大 Key 用 UNLINK

面试标准回答

text
Redis 过期删除和内存淘汰不是一回事。过期删除是 key 到达 TTL 后被删除,Redis 通过惰性删除和定期抽样删除配合完成;惰性删除是在访问 key 时检查是否过期,定期删除是周期性从过期字典抽样清理,避免全量扫描阻塞。内存淘汰是 Redis 超过 maxmemory 后,根据淘汰策略选择 key 删除,例如 allkeys-lru、volatile-lru、allkeys-lfu、noeviction。TTL 到期不保证立刻删除,大量 key 同时过期可能导致雪崩,所以 TTL 要加随机。生产排查内存高要看 used_memory、expired_keys、evicted_keys、大 Key、无 TTL Key 和淘汰策略。