结论:对排行榜类业务,volatile-lru 不是安全选择,除非你给每个 ZSET 成员都显式设了 EXPIRE;否则未设过期时间的 ZSET 键根本不会被 volatile-lru 触及,内存爆掉时会直接拒绝写入。

直接说结论:对排行榜类业务,volatile-lru 不是安全选择,除非你给每个 ZSET 成员都显式设了 EXPIRE;否则未设过期时间的 ZSET 键根本不会被 volatile-lru 触及,内存爆掉时会直接拒绝写入。
为什么 volatile-lru 对 zset 排行榜基本无效
Redis 的 volatile-lru 只扫描 server.db[i].expires 这个过期字典里的 key —— 也就是说,它只看「有没有设置过期时间」,不看 value 类型。而排行榜常用的 ZSET(比如 rank:week:202605)通常不设 TTL,因为业务上要长期有效或按周/月手动清理。结果就是:volatile-lru 根本找不到可淘汰的 key,触发内存阈值后退化为 noeviction 行为,所有 ZADD、ZINCRBY 都返回 (error) OOM command not allowed when used memory > 'maxmemory'。
- 常见错误现象:
ZADD rank:day:20260526 user1 99突然失败,但INFO memory显示used_memory_human已超maxmemory - 排查关键点:执行
SCAN 0 MATCH rank:* COUNT 1000后对每个 key 跑TTL,会发现绝大多数返回-1(永不过期) - 根本原因:
volatile-lru的作用域是「键是否在过期字典中」,不是「键是否是 ZSET」或「键是否含大量成员」
zset 排行榜该用 allkeys-lru 还是 allkeys-lfu
如果排行榜数据天然有冷热之分(比如日榜活跃、月榜沉寂、年榜几乎只读),allkeys-lru 比 allkeys-lfu 更稳妥。因为 LRU 基于最近访问时间,ZCARD、ZRANGE、ZSCORE 这些读操作会刷新 LRU 时钟;而 LFU 的频率计数对排行榜意义不大——用户刷一次榜单就让整个 ZSET “变热”,但实际只需要保留 top 100,其余几万成员不该因此驻留内存。
-
allkeys-lru的优势:淘汰时能真实反映“哪些 ZSET 最久没被查过”,适合按周期生成的榜单(如rank:hour:2026052608小时榜几小时后就没人读) -
allkeys-lfu的风险:旧榜单只要被某次运营后台拉取过一次,其 LFU 计数就会抬高,导致真正该淘汰的临时缓存反而留下 - 性能影响:
allkeys-lru随机采样默认 5 个 key(maxmemory-samples 5),对大实例建议调到10或20,避免误杀高频小榜单
必须配合的实操控制点
光靠淘汰策略不够,ZSET 本身容易成为内存黑洞:一个含百万成员的 ZSET 即使只有 score+member 字符串,也远超等量 STRING 占用。得从写入侧卡住。
- 强制周期性清理:用
redis-cli --scan --pattern "rank:day:*" | xargs -I{} redis-cli EXPIRE {} 86400给日榜统一加 24h TTL,使其进入volatile-lru作用域 - 限制单个 ZSET 规模:在应用层做拦截,例如
ZCARD rank:week:202605> 10000 时拒绝新增,或自动ZREMRANGEBYRANK rank:week:202605 10000 -1 - 避免 score 冗余:不要用
ZADD rank:user:1001 1672531200 "order_abc"存时间戳当 score,改用独立STRING或HASH存元数据,score 只留排序权重 - 警惕
ZUNIONSTORE:合并多个大 ZSET 时可能瞬时内存翻倍,务必搭配MEMORY USAGE监控和CLIENT PAUSE预留缓冲
最易被忽略的是:ZSET 的 member 字符串长度直接影响内存占用,一个 1KB 的 member 在百万级 ZSET 中就吃掉近 1GB 内存。别只盯着淘汰策略,先砍掉不必要的长字符串字段。


















