外观
实测对比:Redis 八种淘汰策略下 LRU 与 LFU 命中率差多少
为什么 maxmemory 淘汰策略会直接影响缓存命中率
很多业务把 Redis 当作“无限大”的缓存使用:请求先查 Redis,未命中再回源数据库。但线上 Redis 一定会配置 maxmemory,当内存达到上限后,Redis 必须决定哪些键继续保留、哪些键被删除。这个决策由 maxmemory-policy 控制,选错策略时,常见表现是 Redis 内存稳定、CPU 不高,但数据库 QPS 周期性飙升,接口 P99 延迟抖动。
Redis 4.0 之后共有八种淘汰策略:
- noeviction:不淘汰,写入报错;
- allkeys-lru:所有键中淘汰最久未访问的键;
- allkeys-random:所有键中随机淘汰;
- allkeys-lfu:所有键中淘汰访问频次最低的键;
- volatile-lru:仅在设置了过期时间的键中执行 LRU;
- volatile-random:仅在带过期时间的键中随机淘汰;
- volatile-ttl:优先淘汰剩余存活时间更短的键;
- volatile-lfu:仅在设置了过期时间的键中执行 LFU。
这里有一个容易误解的点:Redis 的 LRU 并不是严格维护全局链表的标准 LRU,而是近似 LRU;LFU 也不是简单计数器,而是带时间衰减的 Morris 计数器。因此,只凭算法名称判断线上效果并不够,最好结合访问分布做压测。
八种淘汰策略的决策路径
压测方案:用同一批 Zipf 流量比较命中率
缓存流量通常不是均匀分布,而是明显的二八分布:少量热点键承载大部分读取。为了模拟这种场景,可以准备 20 万个键,每个键约 200 字节,逻辑内存约 40MB;将 maxmemory 设置为 10MB,使 Redis 持续处于需要淘汰的状态。读流量按 Zipf 分布生成,让前 5% 的键承担约 70% 的读取。
先启动一个独立实例,避免和开发机其他 Redis 数据互相影响:
bash
# 启动端口为 6399 的测试 Redis
redis-server --port 6399 \
--maxmemory 10mb \
--maxmemory-policy allkeys-lru \
--maxmemory-samples 5 \
--save "" \
--appendonly no
redis-cli -p 6399 ping
# 输出:PONG写入一组 String 类型测试键。真实业务中 String 常承载用户资料、商品快照、配置 JSON 等缓存数据:
bash
# 写入 user:10001,模拟用户资料缓存
redis-cli -p 6399 SET user:10001 \
'{"id":10001,"name":"alice","city":"Shanghai"}'
# 写入商品缓存,并设置 300 秒过期时间
redis-cli -p 6399 SET item:20001 \
'{"sku":20001,"price":99.00,"stock":32}' EX 300
# 查看键的内部编码,短字符串通常为 embstr
redis-cli -p 6399 OBJECT ENCODING user:10001
# 输出:embstr
# 查看 LRU 时钟相关信息,LRU/LFU 元数据由 Redis 内部维护
redis-cli -p 6399 INFO memory | grep -E "used_memory:|maxmemory:"压测时依次切换八种策略,每种策略执行相同的预热、写入和读取脚本,然后通过 INFO stats 中的 keyspace_hits 与 keyspace_misses 计算命中率:
bash
# 切换淘汰策略,可分别替换为 allkeys-lfu、volatile-lru、noeviction 等
redis-cli -p 6399 CONFIG SET maxmemory-policy allkeys-lfu
# 清空当前测试库并重置统计,仅建议在测试环境执行
redis-cli -p 6399 FLUSHDB
redis-cli -p 6399 CONFIG RESETSTAT
# 执行业务压测后读取命中次数与未命中次数
redis-cli -p 6399 INFO stats | grep -E "keyspace_hits|keyspace_misses"
# 命中率公式:
# hits / (hits + misses) * 100%在我的本地测试中,使用 50 万次读请求、20 万个候选键、10MB 内存限制、采样数为 5,得到的趋势如下。具体数值会受机器、RDB、流水线批量大小影响,但相对趋势稳定:
| 淘汰策略 | 典型命中率 | 适用场景 | 主要风险 |
|---|---|---|---|
| noeviction | 预热后不可持续 | 纯存储、不能误删数据 | 内存满后写入直接失败 |
| allkeys-lru | 88% 左右 | 通用缓存、时间局部性明显 | 历史冷数据偶发访问可能占用名额 |
| allkeys-lfu | 93% 左右 | 热点稳定、热点 Key 明显 | 突发热点短期可能无法快速升温 |
| allkeys-random | 70% 左右 | 访问极度均匀 | 热点被随机删除,回源放大 |
| volatile-lru | 取决于过期键比例 | 永久数据与缓存共存 | 无过期键可淘汰时仍会 OOM 报错 |
| volatile-lfu | 通常略高于 volatile-lru | 可过期缓存中有稳定热点 | 依赖 TTL 设计是否合理 |
| volatile-random | 65%~75% | 临时集合、均匀会话 | 命中率不可控 |
| volatile-ttl | 75% 左右 | 希望尽快刷新快过期数据 | 不等于保留热点,只偏向短命键 |
LRU 与 LFU 的核心差异:时间近,还是频次高
LRU 关注“最近有没有被访问”。如果一个低频键刚刚被访问,它在 LRU 看来就是年轻键;一个长期高频但最近几秒没被读取的键,反而可能被淘汰。因此,在扫描任务、离线任务、批量回源等场景下,LRU 很容易被一次性冷流量污染。
LFU 关注“一段时间内访问了多少次”。Redis 使用 8 bit 记录频率信息,并通过概率递增和时间衰减避免计数器无限膨胀。它更适合商品详情、首页推荐、达人信息这类热点集合相对稳定的业务。压测中,allkeys-lfu 相比 allkeys-lru 命中率高约 4~6 个百分点,核心原因正是少量热键不会因为短时间未访问而被冷扫描挤掉。
但 LFU 不是所有场景都更优。秒杀、突发新闻、直播间商品等场景中,新热点可能在几十毫秒内突然出现,而旧热点频次已经很高。此时如果 LFU 升温速度、衰减周期没有结合业务调整,新键竞争不过旧键。可以通过下面两个参数调整:
bash
# lfu-log-factor 越大,频率增长越慢,默认值为 10
redis-cli -p 6399 CONFIG SET lfu-log-factor 8
# lfu-decay-time 表示频率衰减周期,单位分钟,默认值为 1
redis-cli -p 6399 CONFIG SET lfu-decay-time 5
# 初始化热点键时也可以让 LFU 从较高频率开始,适合明确的热 Key 预热
redis-cli -p 6399 SET item:hot:1 "demo" EX 600
redis-cli -p 6399 OBJECT FREQ item:hot:1需要注意,OBJECT FREQ 只在 LFU 策略下有意义;在 LRU 策略下应观察 OBJECT IDLETIME。
选型建议:不要把 volatile 当成默认万能方案
如果 Redis 中全部是缓存数据,优先考虑 allkeys-lru 或 allkeys-lfu。它们不依赖业务是否正确设置 TTL,即使某些键忘记过期,也能在内存紧张时参与淘汰。热点稳定选 LFU,访问随时间滑动变化选 LRU。
如果 Redis 同时存放不能删除的持久状态和可淘汰缓存,才考虑 volatile-* 策略。例如 Hash 中存放购物车,String 中存放商品缓存,并且只有缓存键设置 TTL,这时 volatile-lfu 可以避免误删购物车数据。但前提是所有缓存键必须统一设置过期时间,否则内存照样可能写满。
noeviction 更适合把 Redis 当作数据库或关键状态存储的场景,而不是高命中缓存。volatile-ttl 则适合“越接近过期越应该先刷新”的缓存刷新模型,它的目标不是最高命中率,而是降低过期瞬间大量回源的风险。
避坑总结:压测与线上配置的 5 个细节
1. 只看内存不看命中率,导致数据库被打穿
常见错误现象是 Redis used_memory 稳定在 maxmemory 附近,但 MySQL CPU 在业务高峰周期性升高。排查发现策略配置成了 allkeys-random,热键被随机删除。修正方法是观察 keyspace_hits/(keyspace_hits+keyspace_misses),在热点明显的业务中改为 allkeys-lfu,并对比至少一个完整业务周期。
2. 使用 volatile-lru 却没有给所有缓存键设置 TTL
报错通常表现为 OOM command not allowed when used memory > 'maxmemory'。原因是 Redis 只在带过期时间的键中淘汰,永久键不参与。可用 TTL key 抽查,并用扫描脚本统计无过期键数量;修正方法是缓存写入统一使用 SET key value EX seconds,或在接入层封装统一缓存方法。
3. 把批量扫描流量直接打到缓存,污染 LRU
数据修复任务顺序遍历大量冷键后,正常热点命中率下降。这是典型的 LRU 污染。修正方式有三种:批量任务走单独 Redis 实例或只读数据库;降低任务并发并错峰执行;将主缓存策略调整为 allkeys-lfu,让冷键不会因一次访问就获得过强保留资格。
4. maxmemory-samples 设置过低却误以为是算法无效
默认采样数为 5,性能较好但不是最精确。压测中把 samples 从 5 提高到 10,LRU 命中率通常会提升,但 Redis 淘汰时 CPU 消耗也会增加。建议先在压测环境比较 5、10 的命中率和延迟,不要一上来在线上设置过大值。
bash
# 提高近似 LRU/LFU 的采样精度
redis-cli -p 6399 CONFIG SET maxmemory-samples 105. 忽略大 Key 对淘汰效果的放大作用
淘汰算法选择的是键,但释放的内存大小可能差异巨大。一个 5MB 的大 Hash 被淘汰可以释放很多空间,而大量小键被淘汰会带来更高删除频率。线上应结合 redis-cli --bigkeys 或内存分析工具治理大 Key,避免单个键过期、淘汰时阻塞主线程。对于热 Key,还应考虑本地缓存、只读副本分担和多副本 Key 分散,不能只依赖 LFU 保留。
最终建议很简单:纯缓存优先 allkeys-lru 或 allkeys-lfu;热点稳定选 LFU,滑动访问选 LRU;缓存与持久数据共存时谨慎使用 volatile-*;任何策略上线前都要用真实访问分布压测命中率、回源 QPS 和 P99 延迟,而不是只看 Redis 是否没有报错。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。