Redis内存淘汰策略深度解析:LRU与LFU原理、对比与实战选型
1. 从一次线上告警说起为什么你的Redis突然“失忆”了那天下午我正在工位上摸鱼突然钉钉群里一阵急促的告警声响起。点开一看是核心业务线的缓存服务告警“Redis实例内存使用率超过95%”。心里咯噔一下这可不是小事。登录监控一看一个用作热门商品信息缓存的Redis集群内存像坐了火箭一样往上蹿眼看就要触顶。按照常规操作我第一反应是检查是否有大Key或者大量无过期时间的缓存堆积。但一番排查下来Key的数量和大小都还算正常过期时间也设置得明明白白。问题出在哪直到我查看了Redis的INFO命令输出在# Memory部分看到了关键信息maxmemory_policy: noeviction。瞬间明白了——这个实例配置的内存淘汰策略是noeviction也就是“不淘汰”。当内存用满后新的写入操作会直接返回错误而我们的业务代码没有很好地处理这个错误导致部分请求失败用户体验到的就是“数据加载不出来”或者“操作失败”。这次事故的根因表面上是内存触顶深层原因是对Redis内存淘汰机制的理解和配置不到位。Redis作为一个基于内存的数据库其高性能的前提是数据都在内存中。但内存是有限的、昂贵的资源不可能无限扩容。当实际数据量超过物理内存时Redis必须有一套机制来决定“牺牲”哪些数据以腾出空间给新的或更重要的数据。这套机制就是内存淘汰策略。它绝不是可以随意忽略的配置项而是保障Redis服务稳定、数据“有选择地”存活的生死线。今天我们就抛开那些枯燥的概念结合实战中的坑把Redis最核心的两种淘汰策略——LRU和LFU掰开了、揉碎了讲清楚。你会明白为什么你的缓存“热”数据可能被误杀为什么有些Key看似重要却留不下来以及面对不同的业务场景你究竟该选哪一个。2. 淘汰策略全景图你的Redis在内存告急时会怎么做在深入LRU和LFU之前我们必须先对Redis的内存淘汰策略有一个全局的认识。这就像打仗你得先知道手里有哪些牌每张牌怎么打。Redis通过maxmemory配置项来设置内存使用上限。一旦使用的内存达到这个限制后续的写入命令就会触发内存淘汰流程。而具体怎么淘汰则由maxmemory-policy这个配置项决定。它主要有以下几类策略第一类不淘汰直接报错noeviction这是Redis的默认策略。当内存不足时所有会申请更多内存的命令如SET,LPUSH,SADD等都会返回错误(OOM command not allowed when used memory maxmemory而只读命令如GET则不受影响。这个策略适用于你把Redis当作纯内存数据库数据绝对不能丢的场景但要求你的应用必须有完善的错误处理机制。像我们开头遇到的线上问题就是反面教材。第二类从所有Key中淘汰这类策略会从整个键空间所有数据库无论是否设置过期时间中挑选受害者。allkeys-lru: 使用LRU算法淘汰最近最少使用的键。allkeys-lfu: 使用LFU算法淘汰最不经常使用的键。allkeys-random: 随机淘汰任意键。allkeys-lfu是Redis 4.0引入的如果你的版本低于4.0则没有这个选项。第三类从设置了过期时间的Key中淘汰这类策略只从那些设置了过期时间TTL的键中挑选。这通常更符合缓存的使用场景缓存数据本来就有生命周期淘汰它们对业务的影响相对可控。volatile-lru: 使用LRU算法淘汰设置了过期时间的键中最近最少使用的。volatile-lfu: 使用LFU算法淘汰设置了过期时间的键中最不经常使用的。volatile-random: 随机淘汰任意一个设置了过期时间的键。volatile-ttl: 淘汰过期时间最近TTL最小的键。这个策略的目标是尽快释放即将过期的内存。这里有一个非常重要的选择逻辑volatile-xxx策略只在有键设置了过期时间时才会生效。如果内存满了但没有任何键有过期时间那么这些策略的行为会退化成noeviction同样会报错。所以如果你选择了volatile-lru请务必为你的大部分缓存键合理设置过期时间。那么面对这么多选项我们该如何选择这里有一个简单的决策流数据能否丢如果你的Redis实例承担了持久化存储的角色例如会话存储、排行榜数据绝对不能丢那么noeviction是唯一选择但你必须确保应用能优雅处理OOM错误并有监控告警。是否是纯缓存如果是那么优先在volatile-xxx策略中选择。这能保证你的永久性数据如果存在绝对安全。缓存访问模式是否明确如果你的缓存有明显的“热点”数据如电商首页商品、新闻头条且希望尽可能保留这些热点那么LRU或LFU是更好的选择。random和ttl更简单粗暴适用于访问模式非常随机或你更关心内存回收速度的场景。是否所有数据都可淘汰如果你把Redis当作缓存并且所有数据都可以被淘汰那么allkeys-xxx策略也是可以的这样能最大化内存的利用率。注意在生产环境中allkeys-random和volatile-random很少被使用因为随机淘汰可能导致重要的热点数据被意外清除引发缓存穿透打垮后端数据库。volatile-ttl在某些特定场景下有用比如你希望尽快腾出空间并且过期时间设置得比较准确。3. LRU算法深度拆解它真的是你以为的“最近最少使用”吗LRU全称Least Recently Used最近最少使用。它的核心思想非常直观如果一个数据最近被访问过那么它将来被访问的可能性也更高。因此当需要淘汰时应该淘汰最久没有被访问的数据。想象一下你书桌的桌面。你最近正在看的书、正在写的笔记本肯定会放在手边内存里。而一本几个月前翻过一下之后再也没碰过的书就会被你塞回书架淘汰掉。LRU就是基于这种“时间局部性”原理。3.1 教科书里的LRU与Redis的“近似LRU”在数据结构教科书里实现一个完美的LRU通常需要“哈希表双向链表”。链表维护了数据的访问顺序最近访问的移到头部最久未访问的在尾部哈希表提供O(1)的查找能力。当需要淘汰时直接移除链表尾部的节点即可。但是Redis没有采用这种经典实现。为什么因为内存开销和性能。每个Redis对象本身已经是一个复杂结构redisObject如果再给每个对象加上前驱和后继指针来组成一个全局链表内存消耗会显著增加。更重要的是每次访问一个KeyGET,SET等都需要修改这个全局链表移动到头部这是一个写操作在Redis单线程模型下会对性能产生不小的影响。因此Redis采用了一种近似LRU算法。它通过在每个Redis对象的redisObject结构体中额外增加一个unsigned lru:22的字段22位在LRU模式下用于存储时间戳。这个字段只有24字节在64位系统中的额外开销非常小。它的工作流程是这样的采样当需要淘汰数据时Redis并不会遍历所有Key而是从键空间中随机抽取一定数量的Key默认是5个可通过maxmemory-samples配置放入一个“候选池”。比较比较池中所有Key的lru字段值即上次访问的时间戳。淘汰淘汰掉池中lru值最小的那个Key也就是候选样本里“最久远”未被访问的。3.2 近似LRU的优劣与配置调优这种近似算法带来了明显的优势内存开销极小每个Key只多了24字节。性能影响小访问Key时只需要更新自己对象的lru字段是纯内存操作速度快。淘汰时的随机采样时间复杂度是O(N)其中N是采样数量而不是键总数速度也很快。但它的代价就是“近似”可能不准确。你可能会淘汰掉一个并不是全局最久未使用的Key而是“那一批样本里”最久未使用的。这会导致淘汰精度下降。这里就引出了关键的配置项maxmemory-samples。它的默认值是5。增大这个值会让淘汰精度提高因为考察的样本更多了更有可能找到真正“最近最少使用”的Key但相应的每次淘汰时的CPU消耗也会增加。这是一个典型的“时间换空间”或“精度换性能”的权衡。那么该设置为多少呢Redis官方提供了一个非常直观的测试结果。他们使用一个符合幂律分布的访问请求流来测试发现samples5时已经能够非常好地近似真实LRU。将sample提高到10精度提升非常有限但CPU消耗几乎翻倍。所以对于绝大多数生产环境保持默认的maxmemory-samples 5是完全足够的不建议盲目调大。除非你有非常确切的证据通过监控发现淘汰精度严重影响缓存命中率并且愿意承担更高的CPU成本。3.3 LRU的经典陷阱与适用场景LRU算法虽然直观但在某些场景下会表现出明显的缺陷“冷数据污染”问题想象一个场景你的Redis内存很大平时只访问其中20%的热点数据。某天有一个一次性的大数据扫描任务比如全量数据导出它依次读取了Redis里所有80%的冷数据。在LRU算法下这些冷数据的lru时间戳全部被更新成了“最新”。当内存不足需要淘汰时之前那20%真正的热点数据因为“最近”没被扫描任务访问反而可能因为lru时间戳更旧而被淘汰掉这就是一次全量扫描“污染”了LRU队列导致热点数据被误伤。周期性访问问题如果一个数据每隔固定的长周期比如一天才被访问一次而其他数据访问更频繁。在LRU看来这个长周期数据每次被访问后都是“最新的”它很难被淘汰即使它的整体访问频率很低。它占着茅坑不拉屎挤占了那些更活跃数据的内存空间。因此LRU更适用于访问模式相对稳定且没有上述“批量扫描”或“超长周期访问”干扰的场景。例如用户会话Session存储。用户最近活跃的会话需要保留。新闻客户端的热点新闻列表最新的新闻总是最热的。一些临时性的、访问时间局部性很强的计算中间结果。实操心得使用volatile-lru时一定要给Key设置一个合理的、不同梯度的过期时间。例如核心热点数据TTL设长些如30分钟普通数据设短些如5分钟。这样即使发生误淘汰也能通过过期时间这个安全网来兜底并且结合volatile-ttl的思想让内存回收更平滑。4. LFU算法深度解析如何量化数据的“热度”LFU全称Least Frequently Used最不经常使用。它的核心思想是如果一个数据在过去被访问的次数越多那么它将来被访问的可能性也越高。因此当需要淘汰时应该淘汰访问频率最低的数据。还是用书桌的比喻。LRU关心的是“你最近一次碰这本书是什么时候”而LFU关心的是“过去一年里你翻看这本书的总次数”。显然一本你经常查阅的工具书高频即使今天没看也不应该被扔掉而一本去年旅游买回来只翻过一次的画册低频就算昨天刚拿出来掸了掸灰也该考虑清理了。LFU算法能更好地应对LRU中提到的“冷数据污染”和“周期性访问”问题。一次性的全量扫描只会让每个冷数据的计数器增加1无法撼动那些计数器成百上千的真正热点数据。长周期访问的数据因为总访问次数低也更容易被淘汰。4.1 Redis中LFU的精妙实现不仅仅是计数器如果只是简单地对每个Key维护一个访问计数器会带来两个大问题空间问题计数器可能会无限增长比如一个全局计数器需要很多位来存储。时间衰减问题一个在过去非常热门但近期已经“过气”的数据会因为历史的高计数而长期霸占内存无法被淘汰。我们需要算法能反映“近期热度”而不是“历史总热度”。Redis的LFU实现自4.0版本起非常巧妙地解决了这两个问题。它复用了redisObject里那个24位的lru字段但这次把它拆分成两部分来用高16位存储一个“分钟级的时间戳”对UNIX时间戳取模精度到分钟。这个时间戳并非最后访问时间而是用于衰减的“上一次衰减时间”。低8位存储一个“访问频率计数器”简称counter。这个counter的最大值就是255。8位计数器最大值255这够用吗Redis通过一个概率递增算法让这个小小的计数器能够有效区分不同热度的Key。counter值越大下一次访问时counter再增加的概率就越低。它的递增逻辑近似于对数增长这意味着从0到1很容易但从100到101就很难。这正好符合我们的直觉区分冷数据和温数据很重要但区分“非常热”和“极其热”就没那么重要了。4.2 热度衰减机制让“过气明星”优雅退场这是Redis LFU实现中最精彩的部分。如果只有计数器一个曾经火爆的Key会永远赖在内存里。Redis引入了“衰减”机制。简单来说如果一个Key在一段时间内没有被访问它的counter值会随着时间的推移而减少。衰减的速度由一个配置参数lfu-decay-time控制单位是分钟。它的默认值是1。算法大致逻辑是根据当前时间与Key的“上一次衰减时间”存储在lru字段的高16位的差值来计算这个Key的counter应该衰减多少。例如lfu-decay-time1意味着如果某个Key在过去1分钟内没有被访问它的counter值就可能被减少。这个衰减不是定时任务全局扫描而是在Key被访问时惰性计算的。当Redis尝试访问一个Key并准备更新其LFU信息时会先根据时间差计算出它应该衰减多少然后再进行可能的递增。你可以通过lfu-decay-time来控制热度的“半衰期”。设置得越大热度衰减越慢历史访问记录影响越久设置得越小衰减越快算法越关注近期热度。通常建议保持默认值1除非你有非常特殊的业务周期特征。4.3 LFU的配置与实战观察与LFU相关的配置主要有两个lfu-log-factor这个参数控制counter递增的难度。默认值是10。因子越大counter增长到相同值所需的访问次数就越多。通常不需要调整。lfu-decay-time上面提到的衰减时间默认1分钟。如何观察一个Key的LFU信息使用OBJECT FREQ key命令。它会返回该Key当前的counter值。这是监控缓存热点、调试LFU策略的利器。LFU的适用场景非常广泛尤其是当你的业务有明显的“热点”数据并且希望缓存能智能地长期保留这些热点时社交网络热点内容明星八卦、爆款文章短期内访问频率极高。电商平台热门商品iPhone新品、秒杀商品在活动期间访问集中。视频网站热门榜单排行榜前列的视频会被反复访问。任何访问模式符合“二八定律”的场景20%的数据承载了80%的流量。踩坑记录我们有一个内容推荐系统使用Redis缓存文章特征向量。最初使用volatile-lru发现每当有运营人员进行后台批量数据分析全量扫描时线上推荐接口的缓存命中率就会骤降响应时间飙升。切换为volatile-lfu后这个问题基本消失。因为批量扫描只会给每个文章增加1点热度无法撼动那些被千万用户点击过的热门文章的高热度值。这个切换带来了显著的性能稳定性提升。5. LRU vs LFU面对业务场景你该如何抉择现在我们对LRU和LFU都有了深入的理解。是时候把它们放在一起做一次全面的对比并给出最终的选择指南了。我们可以从以下几个维度进行对比特性维度LRU (最近最少使用)LFU (最不经常使用)核心思想淘汰最久未访问的数据淘汰访问频率最低的数据关注点访问的时间远近何时访问访问的频率高低访问多少次数据结构在redisObject中存储最后访问时间戳在redisObject中存储频率计数器衰减时间戳内存开销极低 (24位时间戳)极低 (复用24位字段)计算开销低 (更新时间戳)略高 (需要概率计算与衰减计算)抗扫描干扰弱。批量扫描会污染缓存误伤热点。强。单次扫描对计数器影响微乎其微。处理周期性访问差。长周期访问的数据难以被淘汰。好。低频访问的数据容易被淘汰。数据热度稳定性热度变化快。一段时间不访问就会变“冷”。热度变化慢。历史访问次数影响大热度更持久。配置复杂度简单主要调maxmemory-samples。较复杂涉及lfu-log-factor和lfu-decay-time。Redis版本要求所有版本支持。Redis 4.0 及以上版本。选择决策流程图面对一个具体的业务场景你可以遵循以下思路来选择你的数据访问模式是否有明显的、持久的热点比如爆款商品、热搜话题。如果是LFU通常是更好的选择它能牢牢抓住这些热点。你的业务是否存在后台批量任务如数据分析、全量导出如果存在并且这些任务会扫描大量缓存数据那么必须选择LFU来避免LRU的缓存污染问题。你的业务数据是否具有强烈的“最新性”比如新闻Feed、实时排行榜最新的数据就是最热的旧的数据价值迅速衰减。这种情况下LRU的表现可能更符合直觉因为它天然地淘汰旧数据。你的访问模式是否是均匀的、随机的或者没有明显规律如果找不到规律volatile-ttl依赖过期时间或allkeys-random随机淘汰可能是更简单直接的选择但需要警惕缓存穿透。你的Redis版本是否低于4.0如果是那么LFU不可用只能在LRU和其他策略中做选择。一个综合案例一个典型的电商平台其Redis缓存可能分为多种用途商品详情缓存有明显的热点商品iPhone、茅台。访问模式是少数商品被高频访问。推荐使用volatile-lfu并为商品设置合理的过期时间如30分钟。用户会话缓存用户最近活跃的会话需要保留。会话通常有固定有效期且更关注“最近是否活跃”。推荐使用volatile-lru并设置会话过期时间如30分钟。秒杀库存缓存这是一个特殊场景数据极度热点但生命周期极短秒杀结束后即失效。对于这种场景volatile-ttl可能是最合适的因为你可以设置一个很短的、精确的TTL如5分钟让Redis在内存不足时优先淘汰那些即将过期的库存数据既保护了热点又快速回收了内存。6. 实战配置、监控与问题排查指南理论再好不落地也是空谈。这一部分我们直接上命令和配置看看怎么用怎么监控出了问题怎么查。6.1 如何配置与查看淘汰策略配置方法二选一配置文件redis.conf找到maxmemory-policy配置项修改为你需要的策略例如maxmemory-policy volatile-lfu。然后重启Redis或通过CONFIG REWRITE命令重写配置如果允许。运行时动态修改推荐通过CONFIG SET命令在线修改无需重启。这对于生产环境调整非常友好。# 设置最大内存为1GB CONFIG SET maxmemory 1gb # 设置淘汰策略为 volatile-lfu CONFIG SET maxmemory-policy volatile-lfu # 使配置持久化到 redis.conf (如果希望重启后生效) CONFIG REWRITE查看当前策略# 查看所有配置信息中的淘汰策略 CONFIG GET maxmemory-policy # 或者通过 INFO 命令查看 INFO memory # 在输出的 Memory 部分找到 maxmemory_policy 字段6.2 关键监控指标光配置完还不够必须建立监控了解策略的运行效果。内存使用率与淘汰触发通过INFO memory关注used_memory,used_memory_peak,maxmemory以及evicted_keys累计淘汰的Key数量。如果evicted_keys在持续增长说明淘汰正在频繁发生可能需要扩容内存或优化缓存使用。缓存命中率这是衡量缓存有效性的黄金指标。可以通过监控系统计算keyspace_hits / (keyspace_hits keyspace_misses)。命中率下降可能意味着淘汰策略不合理误伤了热点数据。OBJECT命令探查这是一个强大的诊断工具。# 查看某个Key的空闲时间LRU视角下的“冷”程度单位秒。数值越大越久没被访问。 OBJECT IDLETIME mykey # 查看某个Key的访问频率LFU视角下的“热”程度仅当策略为LFU时有效。 OBJECT FREQ mykey6.3 常见问题排查思路问题一缓存命中率突然暴跌。排查步骤检查evicted_keys是否在同期有陡增。如果是说明发生了大量淘汰。确认是否近期有代码发布引入了新的、未设置过期时间的大Key或者改变了数据访问模式。如果使用的是LRU策略回忆是否有后台批量任务如报表生成、数据迁移运行可能导致“缓存污染”。使用OBJECT IDLETIME或OBJECT FREQ抽样检查一些核心热点Key看它们的“热度”是否异常降低。解决方向如果是LRU污染考虑切换到LFU。如果是内存不足考虑扩容或优化缓存数据大小如压缩、拆分大Key。问题二使用了volatile-lru但内存满后依然报OOM错误。排查步骤使用INFO keyspace命令查看expires的数量。如果这个数量为0或极少说明几乎没有Key设置过期时间。使用SCAN命令抽样检查一些Key用TTL命令查看其剩余生存时间。根因volatile-xxx策略只淘汰有过期时间的Key。如果内存中大部分Key都没有设置TTL当内存满时这些策略无Key可淘汰行为就退化成了noeviction。解决方案为缓存Key合理设置过期时间。这是一个必须养成的好习惯。问题三从LRU切换到LFU后预期中的热点数据留存更久了但感觉响应速度有点微小的下降。排查步骤这是正常现象。LFU的计数器更新和衰减计算比LRU单纯更新时间戳要稍微复杂一点会带来极微小的CPU开销。通过INFO stats命令查看used_cpu_sys和used_cpu_user的变化通常这个开销在1%以内对于绝大多数应用是可接受的。权衡用微不足道的CPU开销换取缓存命中率的显著提升和服务的稳定性这笔交易几乎总是划算的。内存淘汰策略不是“设置完就忘”的配置。它需要你根据业务形态进行初选通过监控指标进行验证在业务变化时进行调整。理解LRU和LFU背后的原理能让你在出现问题时不再盲目而是能够有理有据地分析和解决。记住没有最好的策略只有最适合你当前业务场景的策略。