资讯动态

Redis缓存清理实战:内存管理、淘汰策略与安全删除Key指南

发布时间:2026/10/14 7:22:14 来源:尧图企业网站定制
做后端开发和运维的朋友应该都遇到过这样的场景服务没崩但接口突然变慢内存告警一条接一条进服务器一看Redis实例的内存占用已经冲到80%以上。这个时候大家的第一反应通常是赶紧清一下缓存。但清理缓存这四个字听起来简单实际操作过的人都知道这里面的门道比想象中多得多。命令敲错、范围没控制好、Key没删干净轻则缓存雪崩导致数据库被压垮重则线上数据异常一整天都在复盘事故。这篇文章我把Redis缓存清理这件事从头到尾梳理一遍包括底层的内存管理和淘汰机制、手动清理的常用命令、批量清理大Key的方案以及我在生产环境里踩过的一些坑。目标读者是正在接手Redis维护的后端开发、运维同学以及那些对Redis只知道set和get、想系统了解清理策略的朋友。看完之后你至少能回答这几个问题什么时候该主动清缓存、用什么命令清、怎么清才不会出事。1. 缓存为什么要清理先搞懂Redis的内存与Key生命周期1.1 内存模型与内存增长的来源Redis本质上是基于内存的键值数据库所有Key和Value都存在内存里。内存是物理资源总有上限。一个实例默认能用的内存取决于maxmemory配置不配置的话理论上可以吃掉宿主机大部分内存直到触发系统内存压力。所以清理缓存的第一步不是背命令而是先搞清楚内存花在了哪。内存的消耗其实分成几块Key本身和Value本身的字节数、Redis哈希表里每个entry的指针开销一个dictEntry大约是24到48字节根据结构不同有浮动、为高效操作创建的各种内部结构SDS头部、过期字典、起类型编码时的元数据还有主从复制时的backlog缓冲区、客户端输出缓冲区、AOF和RDB持久化在fork时产生的写时复制内存开销。很多人拿业务侧的预估数据去对比监控上的used_memory发现对不上就是因为忽略了这些看不见的开销。举一个我调过的真实例子有个业务往Redis里写了很多类似user:10001的KeyValue是一个几十字节的JSON字符串。表面上看一条数据不到100字节但实际占用内存去到200多字节因为dictEntry、SDS头部、过期时间指针这些全加进去了。几百万条Key堆下来和预估的差距能有几十GB。所以真正线上内存涨得快的通常是这三种情况大Value、没设置过期时间的Key、以及频繁写入产生的大量临时Key。第一种可以直接定位第二种是慢性累积第三种是接口逻辑设计问题。1.2 过期Key的生命周期与清理时机很多人以为设置了expire之后Key在到期那一刻就会被删掉。这个认知不完全对。Redis的过期Key清理是两种策略配合执行的。第一种是惰性删除当客户端访问一个已经过期的Key时Redis发现它过期了才会顺手删除并返回nil。如果一个Key过期后一直没人访问那它在内存里会一直躺着占着空间。第二种是定期删除即使没有客户端访问Redis内部也会每隔一段时间默认每100ms执行一次随机抽一批设置了过期时间的Key检查是否过期过期则删除。但每次抽取的样本数量和耗时都有限制。这意味着如果你的数据量巨大、过期Key分布又很广可能在一段时间内Redis不会把所有过期Key清扫干净。那这部分没来得及清理的过期Key依然占用着内存。这就是明明设置了过期时间内存还是居高不下的一个经典原因。所以做缓存清理之前先把Key的生命周期这个模型搞清楚。你会理解一个关键点清理动作不只是简单的DEL它要配合过期时间设置、内存淘汰策略、甚至业务侧的数据重建机制来设计。只盯着删除命令做文章永远清不干净。2. Redis自己怎么清淘汰策略与内存红线2.1 maxmemory与八种淘汰策略当实例内存达到maxmemory上限Redis不会直接罢工而是根据配置的淘汰策略来决定如何处理新的写请求。官方把淘汰策略分成八种这里先列个表避免有人一上来就看迷糊。策略作用范围淘汰依据适用场景noeviction全部Key不淘汰写命令直接报错只读缓存或希望写失败也不丢数据的场景allkeys-lru全部Key按近似LRU淘汰最久未访问的Key无脑兜底但不推荐volatile-lru只对设置过期时间的Key按近似LRU淘汰常用推荐控制范围在可过期层allkeys-lfu全部Key按近似LFU淘汰访问频率低的Key冷热数据分层明显的场景volatile-lfu只对设置过期时间的Key按近似LFU淘汰热点数据集中在缓存键的场景allkeys-random全部Key随机淘汰临时兜底基本不用volatile-random只对设置过期时间的Key随机淘汰同上volatile-ttl只对设置过期时间的Key淘汰剩余存活时间最短的Key一步一坑这里有一个非常重要的认知淘汰策略不是清理缓存的兜底手段它是应对突发写入内存不足的兜底。真正的缓存清理还是要靠主动控制。为什么因为LRU和LFU在Redis里的实现都是近似算法不是绝对精确。被淘汰的Key是不可预测的你很难知道下一个被淘汰的会不会是热点Key。在生产环境里假如缓存里既有热点数据又有普通数据一旦内存告急触发了allkeys-lru热点Key也可能被淘汰然后在下一个请求时被重新加载。极端流量下这就会形成缓存击穿、雪崩的连环问题。所以我个人的建议是如果一定要配置淘汰策略优先考虑volatile开头的系列。这样Redis在内存紧张时只在可以被淘汰的缓存数据里腾空间业务层那些不允许丢的冷数据或者重要配置只要没设置过期时间就不会被动淘汰。2.2 LRU与LFU的细节与配置建议LRU在Redis里的实现不是精确LRU而是近似LRU。Redis会取一小部分样本从中挑出最久未访问的进行淘汰。样本数量由maxmemory-samples控制默认是5。样本数越大淘汰结果越接近真实LRU但CPU性能开销也越高。这个参数值得专门提一句很多团队只调了maxmemory没调maxmemory-samples结果在内存压力大的时候淘汰效果不如预期很大概率就是采样数太少。LFU则维护每个Key的访问频率信息通过一个衰减算法避免很久以前很热的Key永远不被淘汰。如果你的业务有明显的冷热数据分层LFU通常比LRU更合理如果你只希望简单控制内存上限LRU更好理解也更好排查。配置方面我建议至少在redis.conf里明确三个值maxmemory设置合理上限。一般建议预留整个实例内存的20%到30%给系统和其他内存开销。比如宿主机有32GB内存Redis实例的maxmemory可以设置到20GB到24GB不要贪心顶满。maxmemory-policy明确选择策略。除非业务完全接受任意Key被淘汰否则优先volatile系列。maxmemory-samples线上常用10左右数据量级特别大的实例可以考虑调整到15但要实测性能。这些设置可以通过CONFIG GET和CONFIG SET动态调整修改前务必清楚影响比如把策略从volatile-lru改成allkeys-lru下一次内存淘汰就可能动到未设置过期时间的Key这种线上变更需要走变更评审流程。3. 主动清理缓存命令选型与批量操作实践3.1 DEL、UNLINK和EXPIRE怎么选手动删除指定Key最直接的是DEL。但DEL有一个隐蔽的坑当Key的Value特别大的时候DEL在执行时会阻塞Redis主线程。注意不是慢是阻塞。因为DEL需要同步释放该Key关联的所有内存大Key的释放动作可能持续几十毫秒甚至上百毫秒。在这段时间内其他所有命令全部排队等待Redis看起来就像卡死了一样。所以在Redis 4.0以后引入了UNLINK命令它的删除语义和DEL一样——从键空间中移除Key并返回删除结果但实际的释放内存操作放到后台线程异步执行。使用方式几乎一样UNLINK user:10001返回1表示Key存在且已删除返回0表示Key不存在。在绝大多数场景下我建议直接用UNLINK哪怕Key很小也没关系UNLINK是安全的不会因为Value大小而阻塞主线程。还有一个容易混淆的操作是EXPIRE。它不是立刻删除Key而是给Key设置一个未来过期的时间点。如果你为了清理缓存去执行EXPIRE key 1那其实是在告诉Redis这个Key 1秒后过期。Redis会在1秒后通过过期机制来处理。这个操作和DEL、UNLINK的差异在于EXPIRE适合还想保留Key只是希望它到期后自动淘汰的场景如果你想立刻把数据清掉直接用DEL或UNLINK不要绕弯子。这里提一个真实案例之前有个同事为了省事把缓存清理写成EXPIRE key 0以为0秒就是立即过期。实测发现这个命令并不会把Key删掉部分版本会返回错误部分版本会把Key变成永久Key。这种隐蔽的假清理比不清理还坑因为监控上内存没降大家还不知道错在哪儿。3.2 按Pattern批量删除KEYS的坑与SCAN方案很多刚接触Redis的人想清一批前缀相同的缓存比如所有session:*第一反应是执行KEYSKEYS session:*然后遍历返回结果挨个删除。这个做法在小数据量下没问题但一旦实例里的Key总量达到几十万、上百万KEYS会在主线程里全量扫描键空间整个Redis在扫描期间无法处理其他命令这在生产环境是绝对禁止的操作。所有大厂运维规范里第一条就是生产环境禁用KEYS。替代方案是用SCAN它采用游标的方式分批迭代每次返回一部分Key和下一个游标SCAN 0 MATCH session:* COUNT 1000上面命令里的COUNT不是返回数量的硬上限而是每个桶的扫描工作量参考值实际返回的Key数量可能比COUNT大也可能比COUNT小。这个误解需要纠正很多人以为COUNT1000就一定返回1000条其实只是扫描策略的一个提示。SCAN命令的单次耗时很低不会阻塞主线程所以可以循环调用直到游标回到0。把SCAN配合Shell实现批量删除常见写法是redis-cli --scan --pattern session:* | xargs -L 100 redis-cli DEL这条命令在单实例单连接下没问题但有一个隐患如果Key数量特别多xargs会分批启动redis-cli进程每一批都建立一个新连接效率其实不高。更稳的方式是用redis-cli的管道模式或者直接用Python客户端配合SCAN循环删除。集群模式下要注意单节点SCAN只能扫到当前节点的键空间其他slot里的Key不在返回结果里。集群需要先拿到每个主节点的地址分别对每个节点执行SCAN。这部分后面单独讲。3.3 用Lua脚本保证批量删除的原子性xargs分批调用DEL每一步本身是原子操作但整个批量删除不是原子的——执行到一半出错可能一批Key删了一半业务层就会出现部分缓存已清理、部分还是旧数据的中间状态。如果业务上要求要么都删要么保留可以用Lua脚本在一个原子操作里遍历并删除匹配的Key。一个模板如下local cursor 0 local pattern ARGV[1] local batch tonumber(ARGV[2]) repeat local scan_result redis.call(SCAN, cursor, MATCH, pattern, COUNT, batch) cursor scan_result[1] for _, key in ipairs(scan_result[2]) do redis.call(UNLINK, key) end until cursor 0 return true执行方式redis-cli --eval clean_by_pattern.lua , session:* 1000注意Lua脚本在Redis中运行时会占用主线程所以脚本里单次UNLINK最好配合大Value场景选UNLINK而非DEL同时控制好SCAN的COUNT避免脚本执行时间过长。如果匹配的Key数量特别大脚本耗时可能达到秒级甚至更久这时候建议分批跑或者把任务放在低峰期执行。我自己生产环境里用Lua脚本删除的次数其实不算多因为大多数场景对原子性没那么敏感。但有一种场景必须用Lua删除前需要先判断Key的类型、再决定删除方式这种复杂逻辑用纯命令行很难一次完成脚本化更可控。4. 大Key与冷数据的清理方案4.1 什么样的Key算大Key大Key没有一个绝对权威的定义标准不同团队定义不同。实践中我习惯用这几个参考值来划分单个字符串Value超过10KB。集合类型元素数量超过1000个。集合类型Value的总字节数超过1MB比如一个Hash有大量小字段累计下来体积很大。单个Key在内存中的总占用超过100MB这属于极端大Key。大Key的危害不只是删除时阻塞Redis日常的读写、序列化、主从同步都会受影响。比如一个大Hash做HGETALL时网络输出可能一下就打爆客户端缓冲区主从同步时把这个大Key同步给从节点也可能拖慢同步进度。更隐蔽的是很多Redis的慢查询日志里奇怪的缓慢操作往往都跟大Key有关。4.2 大Key如何安全删除对于字符串类型的大Value直接UNLINK就完事了。对于集合类型直接UNLINK也能删但在一些需要保留Key但清空部分数据的场景就得用到分批次删除的办法。比如一个大Hash外层Key还有业务意义不能整个删掉只想清掉其中大部分字段。思路是用HSCAN分批取字段再逐批HDELredis-cli HSCAN myhash 0 COUNT 100拿到字段列表后redis-cli HDEL myhash field1 field2 field3 ...更稳的方式是把这两步包进Lua脚本每轮取100个字段就HDEL一批循环直到游标返回0。List类型可以用LTRIM逐步裁剪到只剩下指定长度Set用SSCAN加SREMZSet用ZSCAN加ZREM。原理都一样不要一次性加载全量分批操作控制每条命令的影响范围。大Key删除还有一个垃圾桶技巧对超大Key先用RENAME把它改名为huge_key_to_be_deleted然后不急着删等低峰期再用UNLINK删除。虽然RENAME本身也会短暂阻塞但比直接删大Key的阻塞时间短很多。这个技巧在高峰期清理紧急大Key时特别好用因为业务侧对这个Key不存在了的容忍度往往高于Redis卡了几百毫秒的容忍度。4.3 定期清理任务怎么设计建议每个团队都有一套定时清理Redis缓存的机制而不是每次内存告警才临时登录服务器处理。设计定时清理任务时我习惯从四个维度考虑。第一个维度是时间窗口。清理任务必须避开业务高峰一般放在凌晨。但要确认凌晨有没有批量对账、报表类任务也在使用同一个Redis实例不然清理任务会跟业务任务抢资源。我见过凌晨的报表任务把CPU吃满然后清理任务又把内存压力拉高两边互相踩踏的情况。第二个维度是删除批次大小。每轮删除的Key数量、每条命令的执行耗时都要想办法控制。宁可多跑几轮也不要一次性清太多。比如按前缀删除时每轮SCAN的COUNT控制在500到1000每轮之间sleep 100毫秒左右这样即使业务侧对某个Key有强依赖也不会因为瞬间删除大量Key导致缓存雪崩。第三个维度是可观测性。删了哪些前缀、删了多少个Key、释放了多少内存一定要有日志。建议清理前后各记录一次INFO memory里的used_memory这样能算出实际释放空间也能发现删了但内存没降这种异常情况。第四个维度是优雅降级。清理任务本身也要有熔断机制如果某轮删除耗时超过阈值应当停止这一轮剩余操作并告警而不是继续执行直到出问题。这个阈值可以通过历史数据统计得出比如正常一轮SCAN加UNLINK在10毫秒以内如果连续三次超过1秒直接中断任务并通知值班人员。5. 生产环境踩坑实录常见问题与排查思路5.1 内存清理了还报警排查顺序很重要有一种情况特别让人头大日志显示清理任务正常执行删除了几万个Key但监控上内存还是高位。我在实际运维中总结了一套排查顺序比较适合内存占用高但看起来没多少业务Key的场景。第一步看INFO memory里的关键指标redis-cli INFO memory重点看used_memory、used_memory_rss、mem_fragmentation_ratio。碎片率mem_fragmentation_ratio大于1.5说明内存碎片严重这不是Key多导致的而是分配器在频繁分配和释放小块内存时产生了碎片。这种情况下删除Key并不能降低RSS甚至可能越删碎片越严重。处理方式是重启实例让jemalloc重新整理内存布局或者在部署层面预留足够内存缓冲。第二步用redis-cli --bigkeys扫描大Keyredis-cli --bigkeys这个命令会遍历整个键空间统计各类Key中占比大的成员。注意这个命令本身也是全量扫描谨慎在高峰期执行最好在低峰期跑一次。它输出的结果会告诉你哪个Key的Value最大、哪个Hash字段最多、哪个List元素最多。第三步检查有没有没设置过期时间的Key在持续累积。方法是用SCAN遍历所有Key对每个Key执行TTL命令筛选出TTL为-1的Key。TTL为-1说明没有设置过期时间-2说明Key不存在或已过期。这个过程也建议用脚本跑不要手动一个个查。第四步检查客户端连接、输出缓冲区和backlog。比如某个客户端一次性拉取了一个大Key的数据输出缓冲区可能占用大量内存主从复制的backlog也可能占用固定内存。这些东西不看Key根本发现不了。用CLIENT LIST看一眼客户端的输出缓冲区大小就能定位到是不是有客户端在拉大对象。这一套排查下来你才会知道内存到底被谁吃了。清理缓存不能只盯Key其他内存消耗也要纳入视野。5.2 缓存清理引发的雪崩和击穿清理缓存最容易引发的事故是缓存雪崩。比如你通过一个Pattern把某个前缀下的所有Key一次性删除而这些Key正是业务读多写少的流量入口删除后大量请求会同时落到数据库数据库压力瞬间打满。这就是清理成了事故的典型场景。避开的思路有几个大批量删除时把清理时间拉长、批次缩小。比如本来准备1分钟内删完10万个Key改成每批500个Key、每批间间隔200毫秒大概20分钟删完。从业务角度看这20分钟里有少量请求因为缓存命中率下降而回源数据库但数据库能扛得住。删除前做好业务侧的错峰。如果清理的是冷数据影响不大如果清理的可能是热点数据先把业务侧的缓存Key切换成新版本比如加个后缀等流量都打到新Key上之后再删除旧Key。给重要的缓存场景多留一个兜底删除之后如果数据库扛不住至少要有限流和降级开关。很多团队没有这个准备删完缓存数据库被打死只能手忙脚乱扩容。缓存击穿则更像是一个点上的事故某个热点Key正好在清理任务执行时被删掉然后瞬间所有请求全部穿透到数据库。规避方式通常是这几种热点Key不要纳入全量清理的Pattern单独维护一份热点Key名单清理任务执行前先排除这些Key。删完Key后立即做一次缓存预热把数据库里那份数据重新写回Redis。或者用分布式锁保证只有一个请求去重建缓存其他请求在锁等待期间短暂阻塞。5.3 集群环境下的清理一致性在Redis Cluster模式里每个Key根据hash slot分布在不同节点上跨节点的批量清理不能简单地用单节点的SCAN加DEL解决。推荐的做法有三种。第一种对每个主节点依次执行SCAN加DEL。先拿到集群中所有主节点的IP和端口然后写一个循环脚本对每个节点执行批量删除。这种做法的好处是逻辑清晰不容易出错。第二种用redis-cli -c --scan --pattern xxx:*在集群模式下扫描。-ccluster模式会自动跳转到对应节点执行命令但要注意SCAN返回的Key可能来自多个节点后续对每个Key执行UNLINK时-c模式能帮我们路由到正确的slot。实际用下来发现redis-cli -c --scan的可用性还算稳定但大批量删除时还是建议走第一种方案自己控制每个节点的删除节奏。第三种自研方案客户端用CLUSTER SLOTS命令获取slot分布计算每个Key的hash slot再把Key按slot分组路由到对应节点批量执行删除。这个方案优点是精准可控缺点是要写代码。跨槽Key删除如果没处理好最常见的报错是CROSSSLOT Keys in request dont hash to the same slot这是集群对多Key操作的限制。逐Key删除则不会触发这个限制因为每个命令只涉及一个Key。另外主从节点不需要单独处理。删除操作写入主节点后删除命令也会通过主从复制传到从节点。所以清理任务只需要对主节点操作即可。不要同时对从节点也执行一遍DEL那是浪费资源而且如果在从节点上开了只读写操作还会报错。5.4 一个实用的清理脚本模板这里分享一个我常用的临时清理脚本模板核心是参数化方便根据场景调整。#!/bin/bash # 用法: ./clean_redis_prefix.sh redis_host redis_port pattern count batch_sleep_ms HOST$1 PORT$2 PATTERN$3 COUNT$4 SLEEP_MS${5:-100} # 使用SCAN分批取出Key每批之间sleep redis-cli -h $HOST -p $PORT --scan --pattern $PATTERN /tmp/keys_to_delete.txt total0 while IFS read -r key; do redis-cli -h $HOST -p $PORT UNLINK $key total$((total 1)) # 每200个Key暂停一下避免集中删除触发雪崩 if (( total % 200 0 )); then sleep 0.1 fi done /tmp/keys_to_delete.txt echo deleted total: $total这个脚本本身不复杂核心思想是控制删除节奏、控制单次操作的Key数量、每批之间加间隔。真要上线跑建议加上日志和统计输出至少记录开始时间、结束时间、删除总量和删除前后的used_memory。如果Redis中有密码需要在每个redis-cli命令里加-a参数如果有特殊字符的Key名脚本里的命令替换要小心建议用eval加双引号包裹Key名避免被Shell错误解析。这个脚本用在测试环境验证逻辑没有问题之后再在生产低峰期执行。5.5 清理完后注意观察的指标清理任务执行完成不等于这件事就算完了。我习惯在清理后的一段时间里持续观察三个指标。第一个是命中率。缓存清理后命中率会有一个短暂的下降这是正常的。但如果命中率持续低位超过一个业务周期说明清理范围太大了把大量还有热度的Key删掉了业务会对数据库产生持续压力需要及时恢复或者预热。第二个是内存的下降曲线。UNLINK是异步删除内存不会瞬间降下来而是有一个滞后。用INFO memory多采样几次观察used_memory是否有逐步下降的趋势。如果过了很久内存还是不动可能是有碎片问题或者有大量Key的过期时间在清理之后依然有效惰性删除和定期删除还需要时间处理。第三个是慢查询日志。清理过程中的大Key删除可能产生慢查询。如果发现删除阶段有大量慢查询下次做清理时要把批次调得更小、间隔调得更长。用SLOWLOG GET查看最近慢查询就能定位到是哪条命令、哪个Key、耗时多少。我个人在长期运维Redis的过程中有一个很深的体会缓存清理这件事做得好了是日常巡检的一部分做得糙了就是定时炸弹。它不像写业务代码那样有明确的功能边界但它直接影响整个系统的稳定性和数据一致性。每次动手清理之前花十分钟想清楚这几个问题——清理目的是什么、影响范围是什么、失败后怎么回退——比闷头敲命令要值钱得多。最后再分享一个小技巧如果公司有完善的监控平台可以给Redis实例的used_memory配置一个动态阈值告警比如按业务容量的80%和90%分两级告警。80%告警时清理任务还能从容执行90%告警时就需要人工介入排查因为再往上走就可能触发maxmemory淘汰策略到时候就不是你想删谁就删谁的问题了。把这个规则沉淀到团队的运维文档里新接手的人也不容易手忙脚乱。

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

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

免费获取报价 →
↑