先从一个很常见的报警说起某业务线上Redis实例的maxmemory触顶内存曲线连续几天只涨不跌。打开INFO keyspace一看里面带TTL的key有上千万个其中大量key的过期时间早就到了却始终躺在内存里不走。很多人第一反应是把maxmemory-policy改成allkeys-lru结果设置完不到十分钟一批没有设置过期时间的核心数据被淘汰线上直接出现缓存穿透和数据库压力飙升。要解决这类问题得把两个概念彻底拆开Redis的过期删除策略和内存淘汰策略。它们表面上都会让某些key消失但触发条件、作用对象、对业务的影响完全不同。这篇文章从源码行为、配置参数、真实排障三个层面把两者讲透同时整理了我自己踩过的坑和常用的排查命令适合刚接触Redis、或者调过maxmemory但没搞懂原理的读者直接参考。1. 两个场景帮你分清到底是“过期”还是“被淘汰”1.1 场景Akey过期了为什么还占着内存很多人设置了EXPIRE之后都默认Redis到点就会把key清掉。实际上Redis只有在两种情况下才会真正删除过期key第一种是访问到这个key时发现它已经过期顺手删掉第二种是后台定期任务扫描时恰好扫到了它。如果某个过期key一直没人访问、又没被后台扫描扫中它就会继续占着内存直到满足两者之一才被处理。这不是bug而是Redis在CPU和内存之间做的取舍。过期只代表这个key被标记成“不可用”不代表内存立刻被释放。所以当你看到一个实例里expires字段的数值很大同时内存水位一直很高时说明大量过期key还没被真正清理掉这时候问题出在过期删除策略的执行效率而不是内存淘汰策略没生效。1.2 场景B内存没满为什么个别key消失了另一种情况更让人摸不着头脑maxmemory没到TTL也没到但某个key就是读不到了。这时候优先查一下实例是不是配置了allkeys-lru或allkeys-lfu。如果某个时间段写入量突然变大内存短期内触顶过Redis就会按策略从全量key里选“最近最少使用”或“访问频率最低”的数据淘汰跟这个key有没有设置TTL完全没有关系也跟它是否过期没有关系。也就是说key虽然没有过期却因为内存压力被当成了牺牲品。这种事情最容易出现在混合业务场景同一个Redis里既放了缓存数据又放了一些常驻配置配置类key没有设置TTL但长期不怎么被读取一旦内存告急就会被清掉。等真正用到时才读不到而那时你手头又没有备份只能重新初始化。1.3 关键差异对比表对比维度过期删除策略内存淘汰策略触发条件key的TTL到期内存达到maxmemory上限处理对象只针对设置过TTL的过期key按策略选择可能涉及所有key执行时机访问时惰性触发 后台定期扫描写入命令执行前的前置内存检查配置方式Redis内置组合策略无需显式配置maxmemory maxmemory-policy 显式配置失败后果过期key残留内存浪费逐渐严重写入失败或常驻数据被误淘汰这张表已经把核心差异说完了过期删除解决的是“数据该不该算有效”的问题属于时间维度内存淘汰解决的是“内存还有没有地方放新数据”的问题属于容量维度。两者互相独立但生产环境里经常同时出现、一起影响你的缓存命中率。2. Redis过期删除策略为什么是“惰性定期”组合拳2.1 最直观的方案惰性删除惰性删除的实现逻辑很直接当客户端对某个key发起读、写、修改操作时Redis先检查这个key是否设置了TTL、是否已经过期。只要过期了就在这次访问路径上直接删除再返回nil或继续执行后续操作。好处是CPU开销极低只有被访问到的key才会触发检查Redis不需要额外维护一个扫描线程。坏处也很明显如果某个过期key一直没人访问它就会一直存留在内存里占用空间不释放。这就像家里不穿的旧衣服你不去翻衣柜它就一直在角落积灰惰性删除只有在翻到它的那一刻才会把它扔出去。一个两个还好量大了内存占用就非常可观。2.2 看上去完美的方案定时删除定时删除也常被叫作主动删除指的是每个设置了TTL的key在到期瞬间由一个精确的定时器触发删除。从内存角度看这无疑是最理想的因为过期即释放不会留任何残留。但Redis是单线程事件模型所有命令都在一个线程里跑。如果同一时间有大量key到期删除任务就得排队执行这期间正常的读写请求全被堵住。而且为千万级别的key维护精确定时器本身就要消耗额外CPU和内存。所以定时删除在理论层面很干净实际在Redis里根本没法落地。这也是很多初学者在教材里看到“三种过期删除策略”但查看Redis源码却发现只有两类行为的原因。2.3 折中方案定期删除定期删除的思路是不为每个key维护定时器而是让后台任务周期性地扫描一小批带TTL的key发现过期就批量删掉。Redis通过周期任务调用activeExpireCycle函数每次执行时按时间预算从expires字典里随机采样一批key删掉其中已经过期的部分然后结束本轮下一轮再继续。由于是随机采样它没法保证每个过期key都在确定时间内被清掉。如果实例里带TTL的key特别多每次采样又分散在不同数据库分区上残留是很正常的。单独靠定期删除依然解决不了过期key堆积问题但它能把总量控制在一个可以接受的范围内。2.4 Redis的真实组合惰性删除定期删除实际源码里这两个是同时生效的定期删除负责控制过期key的总量访问路径上的惰性删除负责兜底。这样设计最核心的原因就是定时删除在单线程模型下无法支撑海量key而只做惰性删除又会让内存浪费失控。执行频率跟配置项hz有关默认是10表示每秒执行10次周期任务。我踩过一个坑为了“更积极”地清理过期key把hz调到了100结果一台低配置实例的CPU空转明显吞吐量反而下降。对绝大多数场景默认值完全够用。真遇到过期key残留严重优先考虑主动扫描而不是盲目调高hz。2.5 过期key对持久化与主从的影响RDB快照落盘时已经过期的key不会写进快照AOF持久化时一次过期删除会被记录成一条DEL命令。主从复制场景里从节点不会自己主动执行过期删除而是等主节点删除后把DEL命令同步给从节点。所以在主从切换的瞬间从节点上可能短暂存在“逻辑上已过期但物理上还没删”的key。这个问题一般不影响读因为从节点在读取时一样会按过期时间判断过期的key会被当成不存在。但如果你有一个严格依赖“从节点内存必须干净”的监控系统就得知道这个机制别把从节点上残留的过期key当成异常。3. 内存淘汰策略八种策略怎么选先搞懂触发机制3.1 淘汰发生在什么时候内存淘汰和过期删除完全不同它不是后台任务而是一个“前置拦截”动作。Redis在处理每个可能增加内存的写命令比如SET、LPUSH、SADD这类操作之前都会先检查当前内存是否已经超过maxmemory。一旦超过就根据maxmemory-policy尝试淘汰一批key来腾空间腾完再继续执行写命令如果找不到可淘汰的key写命令就直接返回内存错误。只读命令不会触发淘汰比如GET和SCAN只是读取数据和遍历key不会带来新的内存增长压力。另外需要特别注意maxmemory设置为0表示不限制内存所以不配置maxmemory的情况下内存淘汰永远不会触发。很多人以为设置了maxmemory就自动启用了淘汰其实还要看maxmemory-policy的具体值如果策略是默认的noeviction内存达到上限后Redis不会删除任何key而是直接拒绝新写入。生产里最常见的报错就是OOM command not allowed when used memory maxmemory字面意思是这条命令要用的内存超出了上限。这时候第一反应不应该是扩容而是先确认这台实例的淘汰策略是否和业务预期一致。3.2 八种策略逐个拆解可以把常见策略先按“对谁动手”分成两类带volatile前缀的只从设置了TTL的key里选淘汰对象带allkeys前缀的则从整个字典里选不管有没有TTL。再结合LRU、LFU、random、ttl这几种“选择算法”就形成了下面的策略组合策略选择范围选择依据典型用途noeviction不淘汰任何key无有严格数据保留要求宁可写失败allkeys-lru所有key最近访问时间纯缓存业务首选volatile-lru带TTL的key最近访问时间缓存为主未设TTL的数据不想被动allkeys-lfu所有key访问频率存在明显冷热分布的数据volatile-lfu带TTL的key访问频率冷热明显且需要保护常驻数据allkeys-random所有key随机数据没有热点淘汰谁影响不大volatile-random带TTL的key随机过期key之间无差别volatile-ttl带TTL的key剩余TTL最短让“越快到期”的key先走noeviction是默认策略也是让新手最困惑的它不会删key只会在内存满时写入失败。有些团队把它配在缓存实例上大促一到写入直接报错监控没覆盖到错误率等发现时用户数据已经写入失败很久了。给纯缓存业务用的实例不建议选noeviction。allkeys-lru是最常用的缓存策略因为它能清理整个实例里“最近最闲”的数据包括没设TTL的key。正因为这样它也是最危险的如果业务里有些常驻数据没有设TTL又长期不被访问就可能被当成冷数据清掉。使用前必须确认这些常驻数据都能容忍丢失。volatile系列相当于在淘汰前加了一道门槛只允许清理设置了TTL的key因此未设置TTL的key会更安全。但要注意如果带TTL的key数量很少清来清去也腾不出多少空间Redis会退化成类似noeviction的行为写命令还是会报错。使用volatile系列之前务必先确认“可被淘汰的key”覆盖了足够的过期数据。LFU是Redis 4.0之后引入的适合有明显冷热分布的访问模型。LRU的问题是只关心最近一次访问时间一个key被批量任务集中访问过一次之后长期不访问LRU可能还会因为它“最近被碰过”而保护它LFU则通过统计访问频率和衰减周期能更快识别出这种伪热点。选择LFU时Redis会复用key里的lru字段来存频率计数不会显著增加单key内存但对CPU有一点额外开销。volatile-ttl看起来很适合缓存生产里反而用得不多。因为TTL最短的key往往是最新写入的数据它们刚刚产生还没来得及被消费就被清掉下一次访问又得回源反而放大存储压力。3.3 近似LRU与maxmemory-samples参数严格LRU要求在每次访问时维护一个链表结构来记录访问顺序Redis为了节省内存和时间开销并没有这么做而是用采样近似LRU。每次需要淘汰时从字典里随机抽取maxmemory-samples个key默认是5再从中选一个最久没访问的key淘汰。采样数越大近似结果越接近真实LRU但每次淘汰时的CPU消耗也越高。实际项目中如果发现淘汰命中率不理想可以把这个参数从5调大到10或20通常在内存写入频率高的场景下会有改善。但调节参数前先要确认瓶颈确实在“选错淘汰对象”否则只是白白增加CPU开销。Redis 4.0之后引入的LFU实现也受这个采样参数影响淘汰时同样需要抽样再比较频率。3.4 按业务场景给一个可直接抄的配置给一个简单有效的选择逻辑纯缓存任意key可丢maxmemory-policy allkeys-lru配合合理的maxmemory维护成本最低。缓存常驻数据不能动常驻keyvolatile-lru前提是所有可淘汰的缓存数据都设置了TTL。数据不能丢内存满了宁可失败noeviction同时做好监控内存到阈值前提前处理或扩容。冷热分明、数据量大allkeys-lfu并适当调整maxmemory-samples提高淘汰准确率。运行时调整命令CONFIG SET maxmemory 2gb CONFIG SET maxmemory-policy allkeys-lru如果想永久生效在redis.conf里写同样配置再重启实例。注意CONFIG SET是运行时临时生效重启后不保留需要写回配置文件的话还要执行CONFIG REWRITE。我一般先在线上用CONFIG SET快速调整确认业务稳定后再在低峰期把配置固化进文件。4. 两个策略放在一起组合行为才更贴近生产现场4.1 过期key堆积但内存没满不会触发淘汰很多实例的状态是带TTL的key数量极大内存水位也不算低但还没触顶。这时候内存淘汰策略根本没有机会介入所有清理压力都在过期删除策略身上。而定期删除的随机采样覆盖不到那么多离散key惰性删除又只清理被访问的key结果过期key就在内存里“滞留”下来。这种情况最常见、也最安全的处理方式是主动扫描清理。我自己写过一个简单的清理流程用SCAN游标遍历全量key对每个key查询TTL发现TTL小于等于0的就执行DEL。SCAN的COUNT参数可以控制单次返回量比KEYS命令柔和很多不会把整个实例卡住。分批在凌晨低峰执行每批几千个看着CPU曲线没有明显抬升再继续。整个过程不需要改动任何淘汰策略。4.2 内存打满且策略是noeviction写入直接报错当maxmemory-policy为noeviction时内存满后Redis的行为很“硬”新写入返回错误旧的key一个不删。这其实是把Redis当数据库用的场景需要的语义数据有持久化要求不允许未设置TTL的数据被自动淘汰。但代价是业务必须能兜住写入失败。线上见过太多实例把这个策略配错平时没流量时毫无感觉一到活动期写入量大涨错误率瞬间起来日志里全是OOM报错。正确的做法是纯缓存实例用allkeys系列带存储语义的实例才用noeviction并且必须配合监控在内存水位接近阈值时提前触发扩缩容。4.3 volatile系列的保护罩没设TTL的key更安全volatile-lru、volatile-lfu这类策略只从带TTL的key里选淘汰对象这会给未设置TTL的key提供一个保护效果只要内存还有可清的过期key常驻数据就不会被碰。但它隐藏着一个风险点如果业务里带TTL的key占比很低比如大部分数据都设置成了永久有效那么内存满了之后可淘汰的key池非常有限清不了多少空间最终Redis还是会退化成类似noeviction的写入失败。使用volatile系列前建议先统计一下expires数量和总量占比。可以用INFO keyspace快速看每个db的keys和expires数字两个值相除就能估算覆盖率。覆盖率低于三成就要慎重选择volatile策略。4.4 被淘汰不等于过期过期了也不一定立即被淘汰这里是最容易踩坑的地方一个key设置了TTL为10分钟刚写入5分钟就不见了很多人第一反应是“过期时间没到怎么就被删了”。其实它可能是被内存淘汰策略选中了跟过期时间没有半毛钱关系。反过来大量过期key在内存里堆着只要内存没到上限它们也不会被淘汰逻辑清走因为淘汰逻辑只看内存水位和访问特征不看TTL。两个机制互相不替代一个看时间一个看空间。生产环境里这两个事件几乎总是交替发生排查的时候先确认当前实例处于哪个状态再去决定是优化TTL策略、调整淘汰策略还是扩容内存就不会被表面现象带偏。5. 常见问题与排查技巧实录5.1 内存只增不减怎么查先看INFO keyspace里的expires字段确认带TTL的key规模有多大再看INFO stats里的expired_keys这个计数器统计的是累计过期删除次数如果它增长缓慢而expires数值又很大说明大量过期key没有被及时清理。这时候优先做主动扫描清理而不是急着调淘汰策略。同时检查是否存在大量没设TTL的key这类key永远不进入过期删除流程只能靠内存淘汰或人工清理才能释放。我的习惯是写一个巡检脚本每天扫描一次实例里没有TTL的key逐个评估是否漏设了过期时间。5.2 key提前消失怎么查如果遇到TTL没到期但key已经读不到的情况按优先级排查检查maxmemory-policy是不是allkeys-lru或allkeys-lfu它们会淘汰常驻和未过期的数据。看INFO stats里evicted_keys是否在持续增长增长就说明淘汰动作一直在发生。确认实例最近有没有主从切换、重启或持久化恢复RDB缺失或AOF丢失会导致部分key不存在。再回到业务日志排查是否有DEL、UNLINK这类删除命令被误触发。不要在确认前就把锅扣到Redis头上先看淘汰计数和实例事件再用业务日志反向追踪。5.3 用INFO命令快速定位指标位置说明maxmemorymemory内存上限0表示不限制used_memorymemory当前实际内存占用expired_keysstats累计过期删除次数evicted_keysstats累计淘汰次数keyspace_hits / keyspace_missesstats命中与未命中次数expireskeyspace当前库带TTL的key数量如果evicted_keys持续上涨说明淘汰压力很大优先检查容量配置和缓存key是否合理。如果命中率长期偏低说明很多请求都在访问不存在的key这已经超出策略能解决的范畴要回业务侧排查缓存穿透、TTL设置过短之类的问题。这两个计数器是Redis给你发出的信号比任何监控图表都更直接。5.4 一个可复现的小实验想彻底理解淘汰策略最好的方式是自己动手试一次。本地起一个Redis实例redis-server --port 6380然后把内存上限调小并开启allkeys-lruCONFIG SET maxmemory 64mb CONFIG SET maxmemory-policy allkeys-lru接着循环写入不同key的大value比如一次写2MB的字符串写几百个。你会看到写入到一定程度后新的key依然能写入成功而旧的、不常访问的key被持续淘汰INFO stats里的evicted_keys不断上涨这就是淘汰策略在正常工作。把策略换成noeviction重复一遍写到内存满后再执行SET就会直接报错之前的key一个都不会少。相同的写入压力下两种策略的结果完全不同。这个实验强烈建议在测试环境做一遍实际看到的行为比读十篇文章都管用。5.5 线上配置检查清单接手一个新Redis实例时我会按这个清单过一遍maxmemory是否有值不设置就是0等于不限内存内存可能被其他业务挤爆。maxmemory-policy是否符合业务预期缓存类优先allkeys系列存储混合类优先volatile系列或noeviction。可淘汰的key覆盖率使用volatile系列时确认大部分数据都设置了TTL否则策略可能形同虚设。监控是否覆盖evicted_keys、expired_keys、keyspace_hits三个指标都要有曲线和告警。持久化配置是否匹配RDB/AOF的开关、过期key对恢复文件大小的影响都要跟淘汰策略一起看。我接手一台Redis服务器时第一件事不是看配置而是先看evicted_keys和expired_keys两个计数器的增长速率。expired_keys在涨说明过期清理机制在工作evicted_keys在涨说明内存压力已经传导到业务数据上这是需要立即处理的信号。很多人把精力纠结在选了哪个策略上其实真正决定体验的是TTL设置是否合理、内存上限是否符合业务峰值。策略只是最后的兜底不是优化工具。命中率不断下滑、淘汰量持续增长时再好的LRU配置也掩盖不了容量规划的问题。
阅读完成 · 觉得有帮助?