"明明设置了过期时间,为什么内存还是被打爆了?"——这是去年我们在一个日均千万级UV的推荐系统里遇到的灵异事件。Redis集群频繁触发内存告警,但监控显示所有key都设置了24小时TTL。你以为的"自动过期"可能根本没按你想的方式工作。
现象:悄悄膨胀的Redis内存
项目背景:一个基于用户行为的实时推荐服务,用Redis存储用户最近24小时的行为特征(key设计为user_behavior:{user_id})。理论上,每天的数据会自动淘汰,内存占用应该保持稳定。
但上线两周后,运维突然告警:Redis内存使用率突破80%阈值。用redis-cli --bigkeys分析,发现大量"本该过期"的key仍然存在。手动执行TTL命令检查,居然返回-1(永不过期)!而我们明明在代码里写了EXPIRE:
// "错误"的写法(但看起来完全合理)
public void saveUserBehavior(long userId, String behavior) {
String key = "user_behavior:" + userId;
redisTemplate.opsForValue().set(key, behavior);
redisTemplate.expire(key, 24, TimeUnit.HOURS); // 设置24小时过期
}
根因:SET+EXPIRE的原子性漏洞
问题出在两步操作的非原子性上。在高并发场景下,可能出现:
- 线程A执行
SET key value - 线程B执行
DEL key(比如用户主动清除记录) - 线程A继续执行
EXPIRE key 86400
此时key已经被删除,EXPIRE实际上作用在了一个不存在的key上,自然不会生效。更讽刺的是,这种竞争条件在测试环境几乎无法复现——需要特定并发时序才会触发。
Redis的SET+EXPIRE不是事务操作,中间可能被其他命令插入。你以为的"设置值并立即设置过期时间",在高并发下可能变成"设置值→其他操作→设置过期时间失败"。
解决方案:原子操作才是王道
正确做法是用Redis的原生原子操作,一个命令完成SET和EXPIRE:
// 正确的原子操作
public void saveUserBehavior(long userId, String behavior) {
String key = "user_behavior:" + userId;
redisTemplate.opsForValue().set(key, behavior, 24, TimeUnit.HOURS); // 单命令原子操作
}
或者用SETEX命令(注意Java客户端封装在setIfAbsent等方法里):
// 另一种原子写法
Boolean result = redisTemplate.execute((RedisCallback<Boolean>) connection -> {
byte[] keyBytes = redisTemplate.getKeySerializer().serialize(key);
byte[] valueBytes = redisTemplate.getValueSerializer().serialize(value);
return connection.setEx(keyBytes, 86400, valueBytes);
});
性能对比:原子操作不仅更安全,还能减少网络往返(1次 vs 2次RTT)。实测在千级QPS下,这种改动能降低约15%的Redis负载。
更深层的坑:你以为过期就真的删除了?
解决了原子性问题后,内存仍然有小幅增长。进一步排查发现:Redis的过期删除是惰性+定期两种策略:
- 惰性删除:只有访问key时才会检查并删除已过期的key
- 定期删除:Redis每10秒随机检查20个key,删除其中过期的
这意味着如果没有主动访问,大量已过期的key可能长期占用内存,直到下一次定期扫描碰巧选中它。对于冷数据,这个"时间差"可能长达数小时。
解决方案:
- 对重要key启用主动扫描:
# 定期扫描匹配模式的key,触发被动删除
redis-cli --scan --pattern "user_behavior:*" | xargs redis-cli ttl
- 适当调高定期删除的频率(修改redis.conf):
hz 10 → hz 100 # 提高后台任务执行频率
active-expire-effort 1 → active-expire-effort 10 # 增加CPU消耗更积极地删除
避坑清单:关于Redis过期的那些坑
- TTL的单位陷阱:
EXPIRE单位是秒,PEXPIRE是毫秒,客户端封装可能不同(比如Java的TimeUnit要明确指定) - DEL会清除过期时间:在key过期前手动执行
DEL,再执行SET会导致key永不过期 - RENAME的副作用:重命名key会继承原key的过期时间,但若新key已存在,会丢弃原有过期时间
- 持久化时的坑:AOF模式下,过期key删除操作会追加到AOF文件;但RDB持久化时,已过期但未删除的key会被持久化
总结与讨论
Redis的过期机制就像瑞士钟表——精密但需要理解其运作原理。核心经验:任何非原子操作在高并发下都可能失效,对关键操作要追问"如果在这一步被打断会怎样?"
你在项目中还遇到过哪些Redis的"隐藏规则"?欢迎分享你的踩坑经历。