Redis键过期失效?这个坑我踩得明明白白

"明明设置了过期时间,为什么内存还是被打爆了?"------这是去年我们在一个日均千万级UV的推荐系统里遇到的灵异事件。Redis集群频繁触发内存告警,但监控显示所有key都设置了24小时TTL。你以为的"自动过期"可能根本没按你想的方式工作。

现象:悄悄膨胀的Redis内存

项目背景:一个基于用户行为的实时推荐服务,用Redis存储用户最近24小时的行为特征(key设计为user_behavior:{user_id})。理论上,每天的数据会自动淘汰,内存占用应该保持稳定。

但上线两周后,运维突然告警:Redis内存使用率突破80%阈值。用redis-cli --bigkeys分析,发现大量"本该过期"的key仍然存在。手动执行TTL命令检查,居然返回-1(永不过期)!而我们明明在代码里写了EXPIRE:

java 复制代码
// "错误"的写法(但看起来完全合理)
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的原子性漏洞

问题出在两步操作的非原子性上。在高并发场景下,可能出现:

  1. 线程A执行SET key value
  2. 线程B执行DEL key(比如用户主动清除记录)
  3. 线程A继续执行EXPIRE key 86400

此时key已经被删除,EXPIRE实际上作用在了一个不存在的key上,自然不会生效。更讽刺的是,这种竞争条件在测试环境几乎无法复现------需要特定并发时序才会触发。

Redis的SET+EXPIRE不是事务操作,中间可能被其他命令插入。你以为的"设置值并立即设置过期时间",在高并发下可能变成"设置值→其他操作→设置过期时间失败"。

解决方案:原子操作才是王道

正确做法是用Redis的原生原子操作,一个命令完成SET和EXPIRE:

java 复制代码
// 正确的原子操作
public void saveUserBehavior(long userId, String behavior) {
    String key = "user_behavior:" + userId;
    redisTemplate.opsForValue().set(key, behavior, 24, TimeUnit.HOURS); // 单命令原子操作
}

或者用SETEX命令(注意Java客户端封装在setIfAbsent等方法里):

java 复制代码
// 另一种原子写法
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可能长期占用内存,直到下一次定期扫描碰巧选中它。对于冷数据,这个"时间差"可能长达数小时。

解决方案:

  1. 对重要key启用主动扫描:
bash 复制代码
# 定期扫描匹配模式的key,触发被动删除
redis-cli --scan --pattern "user_behavior:*" | xargs redis-cli ttl
  1. 适当调高定期删除的频率(修改redis.conf):
properties 复制代码
hz 10 → hz 100  # 提高后台任务执行频率
active-expire-effort 1 → active-expire-effort 10  # 增加CPU消耗更积极地删除

避坑清单:关于Redis过期的那些坑

  1. TTL的单位陷阱 :EXPIRE单位是秒,PEXPIRE是毫秒,客户端封装可能不同(比如Java的TimeUnit要明确指定)
  2. DEL会清除过期时间 :在key过期前手动执行DEL,再执行SET会导致key永不过期
  3. RENAME的副作用 :重命名key会继承原key的过期时间,但若新key已存在,会丢弃原有过期时间
  4. 持久化时的坑:AOF模式下,过期key删除操作会追加到AOF文件;但RDB持久化时,已过期但未删除的key会被持久化

总结与讨论

Redis的过期机制就像瑞士钟表------精密但需要理解其运作原理。核心经验:任何非原子操作在高并发下都可能失效,对关键操作要追问"如果在这一步被打断会怎样?"

你在项目中还遇到过哪些Redis的"隐藏规则"?欢迎分享你的踩坑经历。

相关推荐
data analyse 4561 小时前
数据合规的第三方SDK清单怎么管理?
大数据·人工智能
web打印社区1 小时前
汽修 / 4S 店:维修工单、结算单网页怎么静默出纸
开发语言·前端·javascript·chrome·ecmascript
Dawson Zhu1 小时前
AI Agent架构选型建议
人工智能·语言模型·架构·aigc·agi
蜗牛互联网1 小时前
HSTU在Dynamo-Triton中的AOTI与KV缓存验收方法
java·人工智能·后端·缓存
青山木1 小时前
秒杀系统设计(一):需求拆解与流量治理
java·数据库·redis·后端·架构
TK泰妞1 小时前
跨境电商用AI做TikTok带货内容的实用工具
人工智能
鲜于言悠9051 小时前
阿里云全球扩区:解开AI产品出海的合规与性能难题
人工智能
高升说1 小时前
无人机避障方案取舍:单目、双目与激光雷达的工程对比
人工智能·无人机