资讯动态

Redis过期机制与内存淘汰策略全解析:从原理到生产实践

发布时间:2026/10/3 2:55:47 来源:尧图企业网站定制
做Redis开发或者运维的朋友十有八九都遇到过下面两种场景一种是明明给Key设置了TTL过期时间到了内存却不降反升排查半天找不到原因另一种更急人Redis在业务高峰期突然开始报OOM command not allowed when used memory maxmemory读写全部卡住接口立刻跟着抖成筛子。这两种现象的根源都指向同一个知识盲区——Redis的Key过期策略与内存淘汰机制。这篇内容是《Redis小技巧》系列的第11篇我把日常运维和源码阅读中沉淀下来的东西完整梳理一遍过期Key到底是怎么被删除的、八种内存淘汰策略各自的脾气、生产环境里maxmemory-policy到底该怎么选以及主从复制、大Key、热点Key这些场景下踩过的坑。无论你是刚把Redis引入项目的后端开发还是正在排查内存告警的运维这篇文章都值得认真过一遍。1. 过期Key删除的三种姿势惰性删除、定期删除和那个被放弃的定时删除很多人对Redis过期策略的理解停留在“到了时间就删”这个层面但真实的实现远没有那么简单。Redis对过期Key的处理从来不是靠一个全局时钟挨个扫描而是通过一套组合拳来平衡CPU和内存的开销。1.1 惰性删除用的时候才发现它死了惰性删除Lazy Expiration的逻辑非常直白当客户端请求一个Key时Redis会先检查这个Key是否已经过期如果过期就立即删除然后返回nil。这个检查发生在每次读写命令处理之前对应源码里的expireIfNeeded函数。它是Redis处理命令的主路径上的必经环节所以对性能的影响被压到了最低。惰性删除最大的优点是节省CPU——不管有多少过期Key只要没被访问到就一个都不删。但它的缺点同样明显那些写进去之后再也没有人访问的过期Key会一直躺在内存里占着地方。这就是为什么你给Key设置了过期时间内存却不降反升的现象之一。举个例子你往Redis里写入了10万个Key每个TTL都是60秒但之后的请求只集中在其中1000个Key上。到了第120秒那9.9万个没被访问的Key其实早就该被删掉了但因为没有请求触达它们它们依然占着内存。只靠惰性删除内存迟早要被这些“尸体”撑爆所以必须有第二重机制兜底。1.2 定期删除Redis自己动手的扫地机制定期删除Active Expire是Redis在后台默默执行的主动清理机制。它由serverCron周期函数驱动按照server.hz的配置频率默认每秒10次触发activeExpireCycle逻辑针对每个数据库的过期字典进行采样清理。核心算法可以理解成这样一段伪代码for (j 0; j dbs_per_call; j) { // 随机抽取20个带过期时间的key num dictScan(db-expires, ...); while (num--) { // 逐个检查是否过期过期则删除 } }注意“随机抽取”这四个字这是Redis刻意为之的设计。因为数据库里的Key可能多达成百上千万如果每次全库扫描主线程就要卡死。所以它采取的是水库抽样思路每次抽查一批Key删除其中过期的部分如果过期比例仍然超过25%就继续执行下一轮循环直到时间预算用完或者过期比例降下来。这样做的好处是清理成本固定可控不会因为Key总量大而导致主线程阻塞。缺点则是删除存在延迟一个Key在物理意义上可能已经在内存里多存活了一段时间。我用一个具体例子来帮大家理解假设库里有100万个带有TTL的Key其中50万个已经过期。activeExpireCycle每次抽样20个Key删除其中过期的如果本轮过期率超过25%就继续下一轮。这个循环会一直执行直到CPU时间预算耗尽。也就是说如果一个Key恰好在抽样范围之外它可能要到下一次清理周期才会被真正删除甚至在极端的低访问场景下会“赖”在内存里比较长的时间。1.3 为什么Redis放弃了定时删除理论上还有一种更“干脆”的策略定时删除。为每一个设置了过期时间的Key创建一个计时器时间一到立刻删除。听起来很完美但在工程上完全不可行。原因很简单如果大量Key的过期时间集中在一个时间窗口Redis就需要在同一时刻触发海量定时器这会让CPU瞬间飙到满负荷直接把主线程拖垮。Redis的设计哲学从来都是“为了多快好省可以牺牲一点精确性”。定时删除虽然删除时机最精准但对CPU的冲击不可控而惰性删除加定期删除的组合保证了CPU开销相对平稳内存回收虽有延迟但不会造成灾难性的性能问题。这个取舍在源码层面体现得非常充分。2. 内存淘汰当Redis真的住不下了之后会发生什么过期策略解决的是“Key到了时间怎么被清理”的问题而内存淘汰机制解决的是另一个完全不同的问题——当Redis的内存使用量达到了maxmemory上限时新的写入请求该怎么办。2.1 触发条件与默认行为首先明确一个前提内存淘汰策略只有在配置了maxmemory之后才会生效。如果在64位系统上不配置这个参数Redis默认使用无限内存直到操作系统把它杀掉。当maxmemory配置好后一旦Redis的内存占用达到上限每次写入命令之前它都会尝试执行内存淘汰逻辑。这里有一个多数人容易忽略的细节默认配置是noeviction——不淘汰任何Key而是直接拒绝写入。你会在日志里看到经典的报错(error) OOM command not allowed when used memory maxmemory.这个报错的意思是当前内存已经用满了而且策略不允许淘汰任何数据所以写入请求只能被拒绝。很多从缓存中间件转过来的朋友会把Redis当成无限容量的KV存储来用直到线上出现这个OOM报错才开始着急。实际上noeviction策略在生产环境的缓存场景下几乎没有使用价值但对于纯数据库使用场景也就是不允许任何Key丢失的场景它反而是最安全的选择。2.2 八种淘汰策略的行为差异Redis提供了八种内存淘汰策略可以通过maxmemory-policy配置项切换。它们之间的核心差异有两个维度一是淘汰范围是全局还是只在设置了过期时间的Key里选二是选择被淘汰Key的算法是什么。策略淘汰范围选择算法适用场景noeviction不淘汰无写入报错不允许丢数据allkeys-lru所有KeyLRU近似算法纯缓存场景冷热数据分明volatile-lru仅已设置TTL的KeyLRU近似算法缓存和持久数据混合allkeys-random所有Key随机读写均匀冷热不明显volatile-random仅已设置TTL的Key随机很少使用volatile-ttl仅已设置TTL的KeyTTL最短优先优先淘汰即将过期的数据allkeys-lfu所有KeyLFU近似算法有热点Key的缓存场景volatile-lfu仅已设置TTL的KeyLFU近似算法有热点Key且混合存储这里需要解释一个很关键的概念volatile开头和allkeys开头之间真正的分歧。我记得有次和同事讨论一个缓存在线抖动的问题他坚持用volatile-lru因为“只淘汰有TTL的Key更安全”。但线上缓存Key全都设置了TTL这个选择实际效果等同allkeys-lru。真正会出现明显差异的场景是——某些Key是长期存活的业务数据不带TTL同时又有大量带TTL的缓存数据。如果此时maxmemory被打满volatile策略不会碰那些永久Key而allkeys策略则会一视同仁地按访问热度淘汰。再展开讲讲volatile-ttl。它和直觉一致优先淘汰剩余存活时间TTL最短的Key。这个策略在语义上很有吸引力但存在一个隐患——如果某个Key的TTL即将耗尽但同时它又是当前的高频热点Key淘汰它会导致一次缓存击穿。判断是否要用volatile-ttl关键看你的业务是否允许“越接近过期的数据越先被淘汰”这个规则。2.3 近似LRU和LFU的实现细节很多人以为Redis的LRU就是教科书里的标准LRU——用一个链表维护访问顺序。但Redis是单线程模型如果每次访问Key都把节点移到链表头部这个操作本身就会成为性能瓶颈。所以Redis实现的是近似LRU。它的做法是在需要淘汰Key时随机抽取maxmemory-samples个Key默认配置为5选择其中最久没有被访问的一个进行淘汰。采样数越大淘汰结果越接近真实LRU但CPU开销也越高。这个参数值得单独调校。默认的5个采样在大多数场景下够用但如果你发现热数据仍然频繁被淘汰可以适当上调到10。我实测过将采样数从5提升到10的效果在QPS几千的缓存服务上CPU几乎无感知但热Key存活率明显改善。Redis 4.0引入了LFULeast Frequently Used算法它考虑的不再是“多久没被访问”而是“单位时间内被访问了多少次”。这是为了对抗一种典型的LRU失效场景某个Key在很短的时间内被高频访问后之后长时间不再访问这个Key在LRU算法下会被快速淘汰但如果是周期性地高频访问LRU就会反复误杀它。LFU用的是一个带衰减机制的计数器核心参数有两个lfu-log-factor控制计数器增长速度lfu-decay-time控制计数衰减时间。默认情况下一个Key在240秒内没有被访问计数器就会衰减。这也是为什么LFU更适合那些“访问频率稳定”的业务场景。3. 实战选型maxmemory-policy到底应该怎么配这一节直接给干货聊透生产环境里怎么选淘汰策略、怎么设maxmemory。3.1 纯缓存场景的经典配法如果你的Redis只做缓存——也就是说所有Key都可以接受丢失数据库是最终数据源——我推荐直接使用allkeys-lru。maxmemory 4gb maxmemory-policy allkeys-lru maxmemory-samples 10这个配置背后的逻辑是缓存服务的本质诉求是“在有限的内存里最大程度留住热数据”。采用allkeys范围可以让Redis在海量Key中自由选择淘汰对象而不是被“是否带TTL”这个属性捆住手脚。如果你使用的是Redis 4.0以上版本且业务中存在明显的热点Key比如秒杀商品、热门文章这类访问频率远高于平均值的Key可以改用allkeys-lfumaxmemory-policy allkeys-lfu不要迷信LFU是“更好的算法”它只是适合某些特征的数据。如果数据分布很均匀LFU的计数器维护反而没有优势。3.2 混合存储场景的边界策略如果你的Redis里既有缓存Key又有不能丢的业务数据比如分布式锁、任务状态、配置信息情况就复杂一些。这些关键数据通常不设置TTL如果使用allkeys策略理论上是可能被淘汰掉的。在这种场景下有两种做法。做法一是使用volatile策略为所有缓存Key设置TTL核心数据不设TTLmaxmemory-policy volatile-lru这样即使内存触顶Redis也只会从带TTL的缓存Key中挑选淘汰对象核心数据的安全性有了保障。做法二是对“不允许淘汰的任何Key”保持noeviction策略同时做好容量监控。这种做法一般用于Redis被当作可靠存储使用的场景但代价是内存满了之后写入直接失败需要靠监控和扩容来兜底。我个人在实际生产中更推荐做法一。它结合了缓存和数据存储的双重需求只要你在写入缓存时按规定设置TTLvolatile-lru就能在绝大多数情况下满足需求。3.3 动态调整策略的方法maxmemory-policy是可以在线调整的不需要重启Redis# 查看当前策略 config get maxmemory-policy # 动态切换策略 config set maxmemory-policy allkeys-lru # 若要持久化到配置文件 config rewrite我遇到过一个误改配置的案例。某次线上值班同事想把缓存策略改成LFU直接执行了config set maxmemory-policy allkeys-lfu结果没注意到当时Redis的内存使用率已经超过95%。切换后的瞬间Redis开始按新的策略批量淘汰Key由于LFU的计算需要一定访问积累大量热Key在切换初期被误判为低频Key导致缓存命中率直线下跌数据库差点被打垮。切换淘汰策略一定要选择业务低峰期并且切换前先检查info memory里的used_memory和maxmemory的差距。如果可用余量低于总内存的20%建议先扩容再切换。4. 线上环境最容易踩的坑主从复制、大Key与客户端感知4.1 主从复制模型下从节点为什么不主动删除过期Key这是Redis过期策略里最反直觉的一个点也是很多主从架构故障的根源。在主从复制架构中过期Key的删除动作只会从主节点发起。主节点删除一个过期Key后会生成一条DEL命令并传播到从节点由从节点执行删除。从节点自身并不会主动执行activeExpireCycle去清理自己内存中那些已经过期的Key。更关键的是从节点在响应读请求时会通过逻辑过期检查拦截这些Key。什么意思就是如果你的应用直连从节点读到那些物理上还存在、但按照TTL已经过期的KeyRedis会返回nil让它表现成“已过期”的样子但不会立刻删除它。这个处理方式在源码里对应的是从节点模式下的expireIfNeeded分支逻辑。这个设计看起来没什么问题但一旦发生主从切换就会暴露隐患。当从节点被提升为主节点后它内存里那些逻辑过期但物理未删除的Key瞬间不再有“主节点删除命令”帮它兜底只能等activeExpireCycle慢慢地抽样清理。这个时间窗口内如果有应用请求恰好命中这批Key虽然会得到nil但内存压力并不会立刻缓解。所以在主从架构中建议为主节点和从节点同时开启replica-read-only配置限制写操作并在监控上额外关注从节点的内存变化趋势。如果从节点内存持续上涨往往说明主节点清理过期Key的速度跟不上写入速度。4.2 大Key过期导致的主线程阻塞问题很多人只关心“过期Key会不会被删除”却忽略了“删除过期Key这个动作本身有多贵”。Redis是单线程模型执行DEL命令删除一个包含大量元素的集合是需要逐个释放内存的。如果你有一个包含几百万个哈希字段的Key它的TTL到了主动过期流程在主线程里删除它这个瞬间其他所有命令都要排队等待删除完成。线上表现就是Redis延迟从几毫秒飙升到几十秒甚至更久。我处理过一次线上事故某业务方的排行榜数据用了一个ZSET存了大概几百万个memberTTL到期瞬间整个Redis集群的P99延迟从5ms直接打到3000ms持续了好几秒。就是因为过期删除动作阻塞了主线程。解决方案有两个。一个是在设计阶段就避免超大Key把数据按维度拆分成多个小型Key。另一个是如果大Key已经存在且无法快速拆分用UNLINK命令替代DEL# UNLINK是异步删除 unlink big_key_nameUNLINK命令会将Key的删除动作交给后台线程处理主线程只负责把Key从字典中摘除并立即返回。Redis 4.0之后这个命令非常重要建议所有开发者至少知道它的存在。此外批量设置相同TTL的场景也要警惕。如果你的业务一次性写入大批量Key且这些Key的TTL都设置成同一个时间点那么到了那个时间点activeExpireCycle会连续多轮清理这些过期Key如果Key的体积又不小主线程同样会被拖慢。规避方法很简单给TTL加一个随机偏移量让过期时间分散开。import random ttl 3600 random.randint(0, 600) redis_client.setex(key, ttl, value)这段代码是我在缓存雪崩治理中最常用的方案它同时还能降低缓存雪崩的概率。4.3 淘汰对客户端的影响缓存击穿、雪崩与热Key误杀内存淘汰和缓存过期之所以被并列讨论是因为它们都会导致缓存中数据缺失进而引发后端压力。当Redis因为淘汰策略驱逐了一些Key如果这些Key恰好是热点Key就会出现缓存穿透——大量请求同时打到数据库。这一点在使用了近似LRU且采样数较低默认5的情况下尤其明显因为采样少判断精度低热点Key很容易被误杀。我在做缓存治理时有一条经验在Redis层不要完全依赖淘汰策略来管理热点Key热点Key应该单独设置更长的TTL并且通过白名单机制确保它不会被内存淘汰触及。一个简单的做法是给热点Key的value里加一个固定的“保护”字段再配合定时任务定期刷新TTL让它们永远不会被LRU判定为冷数据。另外如果你在使用Redis集群Cluster模式每个分片节点是独立执行内存淘汰的整个集群的maxmemory等于各分片maxmemory之和。也就是说如果集群有3个分片每个分片设置4GB整个集群最多能存储12GB。源数据分布不均可能会导致某个分片先被打满触发该分片上的淘汰而其他分片还有大量余量。这种场景下除了淘汰策略还需要关注Key的分布均衡性。5. 监控与调优让过期和淘汰行为变得可见、可查、可控这一节聊运维侧的事情。你不能等到OOM报错出现才开始处理应该建立一套可见的监控指标来观察Redis的过期与淘汰状态。5.1 必须盯住的几个info字段通过redis-cli info可以查看Redis的完整运行指标和本主题直接相关的集中在memory和stats两个信息段。redis-cli info memory | grep -E used_memory_human|maxmemory_human|mem_fragmentation_ratio redis-cli info stats | grep -E expired_keys|evicted_keys其中几个字段的含义expired_keys从Redis启动以来累计删除的过期Key总数。如果在没有大量过期Key写入的情况下这个值持续增长且数值较大说明设置的TTL过于激进。evicted_keys从Redis启动以来因内存淘汰而驱逐的Key总数。这个值如果持续增长说明内存压力已经很大缓存命中率很可能在同步下降。mem_fragmentation_ratio内存碎片率。如果这个值持续大于1.5说明Redis内存碎片很多就不仅仅是“写入量太大”的问题了。我见过一个鲜活的案例某客户抱怨“Redis内存一直在涨删的没有涨的快”把expired_keys捞出来一看增长缓慢。但info memory里used_memory却一路走高。排查之后发现这个服务频繁执行SET覆盖同一个大Key每次覆盖都会导致Redis的分配器重新分配内存产生大量碎片。后来加上activedefrag yes配置并重启释放碎片内存立刻降了30%。所以内存上涨不一定是过期Key不清理的问题也可能是内存碎片在作祟。5.2 借助redis-cli排查单Key的淘汰风险如果你想判断某个Key是否处于低访问频率、高淘汰风险的状态可以用OBJECT命令查看它的空闲时间redis-cli object idletime key_name这个命令返回的是Key距上次被访问过去的秒数。数值越大说明它越可能成为LRU淘汰的候选对象。对于核心业务Key如果发现idletime异常增大就要检查客户端访问逻辑是否发生了偏移。另外可以用redis-cli --stat持续观察Redis的实时运行状态redis-cli --stat它会以一秒一次的频率连续输出总连接数、内存占用、命中和过期Key等数据。线上排查时开着这个命令观察几秒钟很多问题比看监控图表更直观。5.3 常见排查链路从现象到根因我梳理了一条自己平时排查“Redis内存异常”的参考链路分享给大家先看used_memory_human和maxmemory_human的差距。如果使用率已经超过80%优先通过redis-cli --bigkeys找出大Key和数量异常庞大的Key类型。再看expired_keys的增量速度。如果增量速度远低于过期Key的构造速度说明主动过期循环来不及处理需要调高hz参数默认10可调整为20-30但CPU会多消耗一些。如果expired_keys增量正常但内存仍然不降检查mem_fragmentation_ratio多半是碎片问题。如果evicted_keys也开始增长检查淘汰策略是否合理采样数是否需要调大以及哪些前缀的Key被淘汰得最频繁。# 临时调高hz参数让activeExpireCycle跑得更勤快 config set hz 20注意hz调高会让Redis的周期性任务执行更频繁适合在内存清理压力大、CPU有富余的场景下临时使用不建议长期保持过高的值。6. 结合项目实践一次缓存雪崩的完整排查复盘讲完原理和参数我用一个实际经历来把前面所有的知识点串起来。那是一个典型的活动预热场景我们的服务把一批运营配置写入了Redis做缓存TTL设置为统一的30分钟。活动开始前数据正常QPS平稳。但活动正式开始的瞬间Redis的缓存命中率从99%直接掉到60%数据库的查询量短时间内翻了几倍接口响应陡增。排查过程是这样展开的。第一步看内存used_memory正常没有达到maxmemory上限排除内存淘汰因素。第二步看过期info stats里的expired_keys在一个时间点突然暴增——这正是那批统一TTL的Key同时到期的那一刻。问题清楚了不是淘汰策略选错了而是业务侧把过期时间设置成了固定值导致所有Key在同一秒过期形成了缓存雪崩。修复方案就是我们前面说的TTL随机化。把所有缓存Key的过期时间在基础值上加一个随机偏移量并且把依赖这个逻辑的代码统一封装到一个工具类里避免后续开发继续踩坑。这个事给我的启发是大多数Redis引发的线上事故都不是某个参数配置错了而是对过期机制的理解没有贯穿到业务代码的设计里。TTL不止是“让Key消失”的开关它直接决定了Redis在处理什么时间点会面临多少清理压力。最后再分享一个小技巧。无论你的团队规模大小我都建议把过期策略和淘汰策略的决策依据写进项目的Redis接入规范里。不要只扔给使用者一个客户端连接方式要明确告诉他缓存类Key怎么设TTL、哪些Key允许被淘汰、哪些Key必须保命、maxmemory-policy是哪种策略。这个文档花不了多少时间但省下的是一张张凌晨两点的故障工单。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑