Redis内存管理:过期策略与淘汰策略全面解析及实战避坑
1. 先把两个策略的边界划清楚一个管过期一个管满员很多人在刚接触Redis的时候很容易把“过期策略”和“淘汰策略”搅在一起。面试时候被问到“Redis内存满了会怎么样”经常有同学张口就说“把过期的key删掉”这个答案其实只答对了一半甚至可以说没答到点子上。我的理解是这样Redis有两条独立的内存管理线一条管“key到了过期时间之后的清理”另一条管“内存已经写满时对新写入的处理”。它们虽然最终都是在释放内存但触发条件、执行路径、配置参数完全不同。过期策略Expire针对设置了过期时间的keyTTL不为-1目标是让dead key按时消失避免占着内存不放。淘汰策略Eviction没有设置过期时间的key或者过期的key还没来得及清理导致内存达到maxmemory上限时Redis必须主动踢掉一些旧数据才能让新数据写进来。另一个容易忽略的点是过期策略不是只做一次就完事它有两层配合后面我会详细拆。而淘汰策略是在maxmemory这个红线上做文章两种策略合起来才是Redis在有限内存里长期稳定运行的关键。实话说单纯背概念很容易但真正要命的是实际生产环境里的组合拳什么场景下过期策略会失效淘汰策略选错了会有什么后果这些问题才是今天这篇想跟大家说透的东西。2. 过期策略的三层机制拆解惰性删除、定期删除、淘汰策略的兜底Redis官方文档里对过期删除的描述非常简洁但实际代码逻辑分三条线。我按执行时机逐个讲。2.1 惰性删除平时不动手读的时候才检查当客户端发起一个GET、SETEX、INCR这类命令时Redis内部会调用一个expireIfNeeded函数逻辑是取key的过期时间如果有如果当前时间已经超过了过期时间点删除这个key并向客户端返回空结果或者执行键不存在时的分支逻辑这就是惰性删除——它不主动扫描、不占额外CPU只有在被访问的那一瞬间才判断是不是过期了。优点是省CPU缺点是如果某个key过期后再也没人访问它会一直躺在内存里这就是常说的“内存泄漏”隐患。举个例子一个缓存key设置TTL是24小时如果业务来源是一次性活动活动结束后没人再读这个key它就会在Redis里长期占着内存永远不会被惰性删除触发。2.2 定期删除Redis自己有个后台巡视任务为了处理上面那种“没人访问就一直留着”的情况Redis实现了定期删除机制。核心代码在activeExpireCycle它每隔一段时间由hz参数控制默认10就会抽样检查一批设置了过期时间的key。这个逻辑有几个关键设计不是全量扫描而是从过期key集合中随机抽取一部分批量检查有执行时间预算每次循环最多运行一定毫秒默认可以跑1ms可配置每次循环结束会记录游标位置下次从上次的地方继续保证最终所有过期的key都会被扫到一句话总结定期删除是惰性删除的补强机制用可控的CPU代价降低过期key残留的几率。2.3 为什么还需要淘汰策略兜底这里有个很多人没想透的问题既然有过期策略理论上过期的key最终都会被清掉为什么还要讨论淘汰策略答案是你可能永远等不到那条清理路径。两个典型场景大量短TTL的key同时涌入定期删除扫不过来惰性删除又没人读内存瞬间被打满大量key本身没有过期时间或者过期时间设得特别长内存增长完全由业务并发量驱动这两种场景下Redis必须有一套在“内存满了但新数据还要写”时如何取舍的规则——这就是淘汰策略的意义。所以淘汰策略不是和过期策略并列的替换方案而是在过期策略来不及处理的窗口期兜住内存红线的最后一道防线。3. 内存淘汰策略的完整图谱8个配置逐个说maxmemory-policy这个配置项一共支持8个值我按从简单到复杂的顺序给它们分组。3.1 不做淘汰noeviction这是Redis默认的策略内存达到maxmemory之后所有写命令SET、LPUSH、SADD等会直接报错业务层会看到类似OOM command not allowed when used memory maxmemory的错误。读命令不受影响。这个策略适合把Redis当纯缓存且内存余量充足的环境或者你认为Redis里的数据全都不可丢、丢任何一个都宁可不写新数据的场景。但实际生产里我基本不推荐用noeviction因为一旦内存临界点被触发业务写入立刻全部失败连锁反应很容易击垮上游服务。3.2 在全体key范围内淘汰这组策略对所有key一视同仁不区分你有没有设置过期时间allkeys-lru按照LRU最近最少使用算法淘汰最久没被访问的keyallkeys-lfu按照LFU最不经常使用算法淘汰访问频率最低的keyallkeys-random随机淘汰key3.3 只在设置了过期时间的key范围内淘汰这组策略只对“生死有期”的key生效永久key不受影响volatile-lru在设置了过期时间的key里用LRU淘汰volatile-lfu在设置了过期时间的key里用LFU淘汰volatile-random在设置了过期时间的key里随机淘汰volatile-ttl在设置了过期时间的key里优先淘汰剩余TTL最短的注意一个细节如果你设置了volatile-*策略但Redis里几乎都是没有过期时间的key那当内存满了、又没有可淘汰的volatile key时Redis的行为会退化到noeviction——直接拒绝写入。这是生产环境最常见的误配之一。3.4 LRU和LFU的区别以及近似LRU机制严格说Redis的LRU不是传统意义上的全量LRU它用的是一个近似LRU算法。Redis会给每个key记录一个24位的lru时间戳精确到秒在需要淘汰时从所有key里随机抽取一批maxmemory-samples控制采样数量默认5在这批样本里挑出最久没被访问的那个淘汰掉。这就是著名的“采样淘汰”思路不维护全局LRU链表只用采样逼近真实LRU。带来两个好处一是内存开销极小二是性能稳定。缺点是它不是绝对精确新写入的key如果运气不好被采样到并选中可能会被误淘汰。LFU则是在LRU基础上增加了一个访问频率计数用Morris计数器近似实现用非线性的方式记录访问次数避免溢出。它对“一个key平时不怎么用、偶尔被大批量访问”的场景判断更准更适合突发业务。策略适用场景核心缺点noeviction纯缓存且内存充足内存满后写失败allkeys-lru各种key都愿意丢弃的通用缓存可能误淘汰刚写入的重要keyvolatile-lru只希望淘汰可再生的缓存key如果volatile key不足会退化allkeys-lfu访问频率差异极大的业务新key初期访问次数少容易被淘汰volatile-ttl明确设置了有效期的缓存数据剩余时间最短的不一定是最该删的4. 关键配置项与选型决策maxmemory怎么设、采样数调多少4.1 maxmemory内存上限怎么算maxmemory决定Redis在什么水位触发淘汰。这个值不是越大越好关键是给操作系统留下运行余地。我的经验是maxmemory 物理内存的 55%~65% 左右剩下的留给系统、Redis自身进程、fork子进程RDB持久化时以及网络缓冲区。如果你用的是云服务器还要考虑同一台机器上是否有其他进程。另外Redis自身还有一个used_memory概念你可以用INFO memory查看里面能看到used_memory、used_memory_rss、mem_fragmentation_ratio内存碎片率等关键字段。设置maxmemory之前先观察几天看清业务实际占用再留20%~30%的余量比拍脑袋定数字靠谱得多。4.2 maxmemory-samples采样数maxmemory-samples影响LRU和LFU的淘汰精度。默认5调到10可以让近似LRU更接近真实LRU但代价是每次淘汰要比较的样本更多、CPU消耗略微上升。我的建议是如果key数量特别大比如千万级以上保持默认5就够了如果内存相对紧张、淘汰比较频繁可以调到10实测对CPU的影响很微小。4.3 策略选型结合具体场景的经验值下面是我在项目里总结出的几种选型套路不是标准答案但可以直接抄作业纯缓存场景所有key都是可再生成的首选allkeys-lru。热点数据天然被保留冷数据被优先淘汰业务也感知不到。缓存持久化数据混存持久化key不可丢用volatile-lru或volatile-ttl并且保证所有可淘汰的key都设置了过期时间。光靠“我不用永久key”的自觉是不够的最好在写入时统一封装一个强制过期时间的工具方法。访问频率分布非常不均匀极少数key扛着绝大部分流量allkeys-lfu比LRU效果好但要关注新key首次写入后访问量还没起来时被误淘汰的风险。数据文件有明确的生命周期比如每24小时一轮任务生成的临时数据volatile-ttl配合EXPIRE设置到点自然淘汰逻辑最简单。配置修改方式运行时可以动态调整# 通过命令直接修改临时生效 redis-cli config set maxmemory-policy allkeys-lru redis-cli config set maxmemory 2gb # 如果要持久化继续执行 redis-cli config rewrite注意config rewrite只会把变化写入redis.conf如果配置项是从启动命令行带入的rewrite同样会覆盖掉。5. 实战中的血泪经验过期和淘汰策略引发的三类事故讲理论容易但我更想把踩过的坑写出来。以下是三个真实发生在生产环境、且都和过期/淘汰策略相关的问题。5.1 事故一大量key在同一秒过期缓存雪崩以前给一个电商项目做活动页缓存当时缓存策略是按“活动开始时间固定偏移量”设置TTL的。活动零点开启所有相关key的过期时间都被设置成当天的结束时间结果第二天零点一过几千个key在同一秒被标记过期缓存层瞬间全部miss数据库在那一秒被打到CPU报警。这个问题的根源不是过期策略本身而是过期时间设置太集中。解决方式很简单对相同业务类型的缓存加上随机偏移量。# Python示例设置随机TTL避免雪崩 import random base_ttl 24 * 60 * 60 ttl base_ttl random.randint(0, 600) cache.set(key, value, exttl)如果你用的是批量设置过期时间的逻辑同理给每个key加一个随机尾部。注意不要把随机范围设太大否则有些key过早失效反而增加缓存穿透概率。5.2 事故二大key过期阻塞Redis主线程有一次用户反馈某个查询突然变慢排查后发现是Redis主线程耗时飙升。定位到最后是一个存了上百万元素的zset key过期惰性删除逻辑在主线程里执行删除操作删除大key释放内存的动作让主线程阻塞了整整几百毫秒。这个坑很深惰性删除和定期删除都是主线程在跑如果被删除的key非常大释放内存会直接卡住所有命令。我记得Redis 4.0之后有个异步删除命令UNLINK但过期删除默认还是同步的。后来我们的应对方式是对可能变大的value类型list、zset、hash单独评估超过一定规模就不设短TTL改成异步删除配合业务层清理把关键命令的慢查询日志SLOWLOG GET打开随时盯着这个问题在Redis 6.0以后有所改善内部增加了异步释放机制的演进但贫血的建议仍然是别让大key过期这是底线。5.3 事故三volatile-lru遇到大量永久key服务直接只读有次给一个内部系统上线新版时负责Redis的同学把maxmemory-policy设置成了volatile-lru但没有检查已有key的过期设置。结果发现业务代码里有一部分数据写的是不带过期时间的比如用户配置类数据。内存打满后volatile-lru找不到可淘汰的key直接退回noeviction模式所有写操作全部报错整个系统进入只读状态。后面我们归纳出一个规律凡是使用volatile-*策略一定要配套监控“当前有多少key是不过期的”或者干脆写一个定期扫描任务把过期集合为空的情况实时暴露出来更稳妥的办法是用allkeys-lru它的淘汰目标永远充足很少出现系统因为无法淘汰而“罢工”的问题6. 状态观测与调优手法怎么确认策略在正常工作光配置对还不够还要能随时看清Redis在做什么。我来分享几个我自己最常看的指标和命令。6.1 核心指标用INFO memory可以拿到used_memory和used_memory_human实际占用的逻辑内存maxmemory和maxmemory_human内存上限mem_fragmentation_ratio内存碎片率值在1~1.5之间比较健康超过1.5说明碎片有点多可以考虑重启或设置activedefrag yesevicted_keys由于淘汰而被踢掉的key数量这个数字是评估淘汰是否频繁的直接依据用INFO stats可以看到expired_keys过期被删除的key数量、evicted_keys淘汰掉的key数量以及keyspace_hits和keyspace_misses。6.2 通过监控判断策略是否合适如果evicted_keys持续上涨说明内存一直处于满负荷状态这时候要看淘汰的对象是不是业务核心数据。我一般这样判断evicted_keys上涨但keyspace_hits稳定淘汰的key是低频冷数据基本健康evicted_keys上涨同时keyspace_hits明显下降说明在淘汰热数据策略选错了或者maxmemory设得太低expired_keys极少、used_memory却一直增长说明定期删除扫不到比如key没有设置过期时间但策略却选了volatile系列有一条命令可以快速查看所有key的TTL分布情况redis-cli --bigkeys虽然这个命令主要用来找大key但它也会输出不同类型key的分布。查过期情况可以写个简单的scan脚本按比例抽查TTL。6.3 动态观测和临时调整线上排查时可以这样操作# 查看当前策略 redis-cli config get maxmemory-policy # 临时切换不用重启 redis-cli config set maxmemory-policy allkeys-lru # 确认改动是否已写入配置文件 redis-cli config get maxmemory-policy注意生产环境的改动要留审计记录不能只靠现场操作。我习惯在切换策略前用监控工具截图保存方便事后复盘。7. 几个更容易被忽略的边界问题7.1 过期时间与主从复制的不一致在Redis的主从架构中主库上的过期key删除之后会向从库同步一条DEL命令从库自己也会维护一份过期字典来保证读请求不会读到已过期的数据。这里有一个老版本里的坑在主从网络分区期间从库如果处理过期逻辑不当可能短暂返回过期数据。Redis的解决方式是从库不会单独执行过期删除而是依赖主库的DEL命令或者自身的时钟判断。注意如果主库长期不可用从库时钟和主库不一致也可能出现数据不一致的情况。我没法在这里覆盖所有边界但建议在高一致性要求的场景下监控主从的master_repl_offset和延迟指标。7.2 淘汰策略不会保证“最不可能用的key一定被淘汰”近似LRU毕竟是采样机制理论上存在淘汰错对象的情况。对数据可靠性要求极高的场景不要依赖淘汰策略来兜底应该靠容量规划和限流来保证内存永远不触线。7.3 Redis 4.0之后的内存碎片整理activedefrag yes可以在内存碎片率达到阈值时自动开启整理。但它和淘汰策略相互独立生产环境开启前要先做压测因为整理过程本身会占用CPU。我一般建议在碎片率超过1.5时才主动启用平时保持默认关闭。8. 个人总结与经验补遗这套体系我在线上是怎么用的说到底过期策略和淘汰策略不是面试题里的两条背答案而是在容量规划、故障演练、监控体系里都要给出明确回答的设计决策。以我维护过的几个业务为例网上最常见的模板配置是maxmemory 2gballkeys-lrumaxmemory-samples 10。对这个方案我的看法是适合大多数中小型缓存场景但并不是最优解。如果业务里存在少量绝对不能丢的数据一些公司会把部分配置态数据也放Redis那我会改成volatile-lru并且强制所有可淘汰key的写入路径都带TTL同时跑一个巡检脚本import redis r redis.Redis.from_url(redis://localhost:6379) # 抽查1万个key中设置过期时间的比例 cursor 0 total 0 expired 0 for _ in range(100): cursor, keys r.scan(cursorcursor, count2000) if not keys: break total len(keys) for k in keys: if r.ttl(k) ! -1: expired 1 print(fscan {total} keys, {expired} have TTL, ratio: {expired / max(total, 1) * 100:.2f}%)如果脚本发现volatile比例接近100%那用volatile-lru才真正安全如果比例偏低说明配置和实际数据形态不匹配早晚要出事故。最后分享一个我自己反复琢磨的结论过期策略关注的是时间淘汰策略关注的是空间但线上稳定性关注的是两者的配合节奏。最好的状态不是天天触发淘汰而是让过期key在定期删除中自然消失淘汰策略只作为极少数瞬间的缓冲。如果你发现生产环境每天都在大量淘汰key第一反应不应该是调大maxmemory而是检查key的过期时间设置是否合理、缓存的数据量是否超出了当初的规划。Redis的内存管理之所以值得花时间吃透是因为它直接决定了你在高并发下到底是“闪电”还是“慢速爬行”。把这些机制在自己脑子里构建成完整闭环再遇到缓存雪崩、内存打满、只读故障你就能快速定位到正确的层而不是在错误的方向上绕远路。