上周排查一个线上广告投放系统的性能问题时发现Redis内存占用居高不下——明明所有缓存Key都设置了24小时过期但凌晨低峰期仍有60%的Key存活。你是不是也遇到过类似情况今天我们就扒开Redis的过期策略看看那些你以为会过期但其实赖着不走的Key到底在玩什么把戏。现象过期Key为何阴魂不散我们在Java服务中通过Spring Data Redis设置了这样的缓存// 设置24小时过期 redisTemplate.opsForValue().set( campaign: campaignId, creativeData, 24, TimeUnit.HOURS );监控显示这些Key的TTL确实从86400秒开始倒计时。但当内存不足时Redis的逐出策略却频繁触发——这说明大量Key实际存活时间远超预期。根因Redis的惰性删除与定期删除Redis的过期清理不是实时的它采用双重策略惰性删除当客户端尝试访问一个Key时Redis会检查它是否过期。如果过期就删除并返回空值。定期删除每10秒默认随机抽取20个Key检查删除其中过期的如果发现超过25%的Key已过期则重复该过程。关键点来了如果某个Key在过期后从未被访问且未被定期删除扫描到它就会一直占用内存数据验证过期Key的真实存活时间我们在测试环境做了组对比实验写入10万个带1小时TTL的Key之后完全不做任何操作。结果检查时间已过期Key比例1小时后12%2小时后68%6小时后99%这说明至少6小时内仍有1%的僵尸Key未被清理那些年我们踩过的坑坑1冷数据长期驻留内存广告系统的历史战役数据访问频次极低但占用40%内存。这些Key就像房间里的隐形家具——平时用不到但搬不走。解法对冷数据启用主动扫描// 用SCANTTL命令定期清理伪代码 try (Jedis jedis jedisPool.getResource()) { String cursor 0; do { ScanResultString scanResult jedis.scan(cursor, new ScanParams().match(campaign:*)); cursor scanResult.getCursor(); scanResult.getResult().forEach(key - { if (jedis.ttl(key) -1) { // 无TTL的Key jedis.del(key); } }); } while (!cursor.equals(0)); }坑2主从复制的延迟陷阱某次从库切换为主库后发现大量已过期的促销Key突然复活。这是因为Redis主从复制时从库不会主动删除过期Key而是等待主库发来DEL命令。解法升级Redis版本3.2并开启replica-expire-delay参数。坑3DEL阻塞引发雪崩曾经用KEYS批量删除过期Key直接打满Redis单线程导致服务崩溃。正确姿势用Lua脚本原子化执行- - 每次最多删除100个过期Key local keys redis.call(SCAN, 0, MATCH, ARGV[1], COUNT, 100)[2] for _, key in ipairs(keys) do if redis.call(TTL, key) -2 then -- -2表示已过期但未删除 redis.call(DEL, key) end end避坑清单与过期策略共舞监控expired_keys指标通过redis-cli info查看已删除Key数量如果长期为0说明清理机制可能失效避免大Key集中过期批量写入缓存时给TTL加上随机抖动如24h ± 10分钟版本差异Redis 4.0前定期删除是单线程扫描高频写入时可能来不及清理别依赖过期事件通知keyspace0:expired可能因系统压力大而丢失事件结语下次当你看到Redis内存报警而TTL显示一切正常时不妨先问一句老兄你的定期删除线程是不是又偷懒了最佳实践对于关键业务永远要有二级保障——要么主动扫描冷数据要么给缓存穿上最后限期的外衣如设置MAXMEMORY策略。你在项目中是怎么处理Redis过期Key的欢迎分享你的实战经验