- "为什么设置了maxmemory,Redis还是把机器内存吃满了?" * 上周四凌晨,我们的实时风控系统突然告警------主节点内存占用突破80%,触发了k8s的OOM Killer。紧急回滚后排查发现,罪魁祸首竟是那个我以为早就吃透的
volatile-lru策略。这里面的水,比想象中深得多。
问题现场:诡异的缓存堆积
场景是这样的:一个日均QPS 3w+的风控服务,用Redis缓存规则引擎的中间计算结果。所有key都设置了24小时TTL,内存上限配置了10GB(maxmemory 10gb),淘汰策略是经典的volatile-lru。按常理,当内存不足时Redis应该优先淘汰最久未使用的带过期时间的key,对吧?
但监控显示,在流量高峰时,Redis内存占用直接冲到了15GB(物理机内存上限)。更诡异的是,redis-cli info显示的evicted_keys竟然是0------这意味着根本没有发生预期中的LRU淘汰!
撕开LRU的伪装
翻遍Redis源码后,我发现了这个反直觉的设计:Redis的LRU根本不是传统意义上的LRU,而是近似LRU。两者的关键区别在于:
- 采样淘汰:Redis每次随机选5个(默认)key,淘汰其中最久未使用的那个,而不是全局扫描。这意味着最该被淘汰的key可能根本不在采样范围内。
- 懒惰处理:淘汰只在写操作且内存超限时触发,后台不会主动清理。如果写入量突然激增,可能来不及淘汰就OOM了。
- TTL优先级陷阱 :
volatile-lru只从设了过期时间的key里淘汰,但如果这类key占比不足(比如我们系统中只有60%的key有TTL),LRU池子会越来越小。
shell
# 用这个命令能看到真实的内存瓶颈点
redis-cli --bigkeys
# 对比内存超限前后的key分布变化
redis-cli info stats | grep evicted
压测数据打脸
为了验证,我用memtier_benchmark做了组对比测试:
| 策略 | 内存超限后淘汰速度 | 最大内存溢出量 | 性能损耗 |
|---|---|---|---|
| volatile-lru | 2k keys/s | 23% | 8% |
| allkeys-lru | 8k keys/s | 5% | 15% |
| volatile-ttl | 5k keys/s | 12% | 11% |
看到没?volatile-lru在内存压力下的表现是最差的。你以为设置了TTL就安全了?当带过期时间的key只占总量的60%时,相当于你用40%的内存空间换来了虚假的安全感。
正确姿势与血泪清单
这是我的最终修复方案:
java
// 错误:依赖Redis自动淘汰
redisTemplate.opsForValue().set(key, value, 24, TimeUnit.HOURS);
// 正确:双重保险 + 监控
if (redisTemplate.getRequiredConnectionFactory().getConnection().info("memory").get("used_memory").compareTo(maxMemory) > 0) {
log.warn("Redis memory pressure detected");
triggerManualCleanup(); // 主动清理冷数据
}
redisTemplate.opsForValue().set(key, value, randomTTL(18, 24), TimeUnit.HOURS); // 避免同时过期
- 避坑指南:*
- 别假设
volatile-lru会及时生效------当带过期key占比<90%时,考虑切到allkeys-lru - 监控
used_memory和evicted_keys的比值,前者达到maxmemory的90%就该告警 - 避免批量写入相同TTL的key,用随机过期时间分散淘汰压力
- 在客户端实现二级防护(如上面的Java代码),别把命运全交给Redis
你以为这就完了?
最讽刺的是,当我翻遍文档才发现,官方早在性能优化建议里写过:
"当需要严格限制内存使用时,allkeys-lru通常比volatile-lru更可靠"
可惜这条提示淹没在配置说明的第七段。你看,有时候最贵的学费不是不懂,而是"自以为懂"。
你在用Redis淘汰策略时栽过哪些跟头?欢迎分享你的战争故事。