Redis 过期键删除策略:为什么你的内存打满了?
提出问题
设了 TTL(过期时间)的 key,到期后就自动消失了?没那么简单。很多开发遇到过:明明设置了 1 小时过期,Redis 内存却一直涨、甚至 OOM。这里的核心问题是:Redis 到底什么时候删除过期 key?如果删除不及时,会不会把内存打满?
分析问题
Redis 的过期删除策略是 惰性删除 + 定期删除 双管齐下,不是定时器逐个扫描。
惰性删除:读的时候顺便删
每次客户端访问一个 key 时,Redis 会先检查它的 TTL。如果已过期,直接删除并返回空。这个机制保证了你不会读到过期数据,但它的缺陷也很明显——如果没人访问,过期 key 就一直占着内存。
c
// 伪代码:Redis 惰性删除逻辑
int expireIfNeeded(redisDb *db, robj *key) {
if (!keyIsExpired(db, key)) return 0;
// 删除过期 key
dbDelete(db, key);
return 1;
}定期删除:后台随机采样
Redis 每 100ms 运行一次 activeExpireCycle,从设置了过期时间的 dict 中随机采样 20 个 key,删除其中过期的。如果过期比例超过 25%,就继续采样,单次最多跑 25ms。
c
// 简化版定期删除流程
void activeExpireCycle() {
do {
sampled = 0;
expired = 0;
// 随机采样 20 个带过期时间的 key
while (sampled < 20) {
key = randomKeyWithTTL();
if (expired(key)) {
deleteKey(key);
expired++;
}
sampled++;
}
} while (expired > sampled / 4); // 过期比例 > 25% 继续
}这俩配合起来,正常情况过期 key 不会长期占用内存。但有个边界场景会出问题。
大量 key 同时过期的灾难
假设你给 100 万个 key 设了完全相同的过期时间。到期那一刻,惰性删除只删被访问的 key,定期删除在 25ms 内疯狂采样删除,但 100 万个 key 根本删不完。结果就是:未过期的新 key 被内存淘汰策略踢掉,而已过期未删除的 key 还在占着内存不释放。这就是缓存雪崩的变种。
python
# ❌ 错误做法:所有 key 同一时间过期
for user_id in range(1000000):
redis.set(f"user:{user_id}", data, ex=3600) # 全在 1 小时后过期
# ✅ 正确做法:加随机偏移
import random
for user_id in range(1000000):
ttl = 3600 + random.randint(0, 600) # 3600-4200 秒之间随机
redis.set(f"user:{user_id}", data, ex=ttl)排查思路
如果怀疑线上 Redis 内存异常增长,按以下步骤排查:
bash
# 1. 看 keyspace 状态
redis-cli INFO keyspace
# 输出类似:db0:keys=1000000,expires=800000,avg_ttl=123456
# 2. 用 scan 抽样看哪些 key 没有 TTL
redis-cli --scan --pattern "user:*" | head -20
# 3. 看内存使用趋势
redis-cli INFO memory | grep used_memory_human
# 4. 看是否有大量过期 key 未被删除
redis-cli --bigkeys # 检查大 key 和 key 分布总结
Redis 的过期删除策略是聪明的工程权衡,但你需要知道它的边界:
- 惰性删除 保证不读脏数据,但依赖访问才能触发
- 定期删除 每 100ms 只跑 25ms,过期 key 太多时删不过来
- 过期时间加随机偏移 是防止批量同时过期的最简单有效手段
- 生产环境记得配
maxmemory和淘汰策略,这是最后一道防线
参考:Redis 源码
src/expire.c、src/evict.c、Redis 官方文档 Key eviction