Redis 过期删除与内存淘汰原理
Redis 里经常设置 TTL:
bash
set user:1001 value ex 3600但很多人不知道 TTL 到期后 Redis 到底什么时候删除 key,也分不清“过期删除”和“内存淘汰”。
这一页解决:
- TTL 存在哪里。
- key 过期后是不是立刻删除。
- 惰性删除和定期删除分别做什么。
- 内存满了为什么会触发淘汰。
volatile-lru、allkeys-lru、noeviction怎么选。- 为什么过期 key 堆积、淘汰频繁会导致抖动。
过期删除和内存淘汰不是一回事
| 机制 | 触发条件 | 目的 |
|---|---|---|
| 过期删除 | key 到达 TTL 时间 | 删除已经过期的数据 |
| 内存淘汰 | Redis 使用内存超过 maxmemory | 腾出空间给新写入 |
一个 key 可以:
- 到期后被过期删除。
- 没到期但因为内存压力被淘汰。
- 没设置 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 常用组合:
- 惰性删除。
- 定期删除。
惰性删除
访问 key 时检查它是否过期。
mermaid
flowchart TD
A["客户端访问 key"] --> B{"key 有 TTL 吗"}
B -- "没有" --> C["正常返回"]
B -- "有" --> D{"当前时间超过过期时间吗"}
D -- "否" --> C
D -- "是" --> E["删除 key"]
E --> F["返回不存在"]优点:
- 简单。
- 只处理被访问的 key。
- 不浪费太多 CPU。
缺点:
- 如果过期 key 再也没人访问,它可能留在内存里。
定期删除
Redis 会周期性抽样检查带过期时间的 key,删除已经过期的。
mermaid
flowchart TD
A["周期任务触发"] --> B["从 expires 字典抽样"]
B --> C["检查是否过期"]
C --> D["删除过期 key"]
D --> E{"过期比例是否较高"}
E -- "是" --> F["继续多做一些清理"]
E -- "否" --> G["结束本轮,避免占用太多 CPU"]为什么是抽样,不是全量扫描?
全量扫描过期字典会阻塞主线程,key 多时风险很大。
为什么过期 key 会堆积
可能原因:
- 大量 key 同时设置相同 TTL。
- key 到期后不再被访问,惰性删除触发少。
- 定期删除抽样清理不过来。
- Redis CPU 忙,清理任务压力大。
后果:
- 内存看起来降不下来。
- 后续访问过期 key 时触发删除,增加延迟。
- 内存压力触发淘汰。
内存淘汰什么时候发生
如果配置了:
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["适合长期热点比较稳定的场景"]例子:
- 新闻热点变化快:LRU 可能更合适。
- 全站配置、热门商品长期热: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推荐策略:
- 设置 TTL,避免旧缓存永久存在。
- TTL 加随机值,避免同时过期。
- 热点商品用逻辑过期或预热。
- 大 JSON 控制大小,避免大 Key。
- 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_memory | Redis 分配的内存 |
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 和淘汰策略。