Redis缓存清理实战:从DEL/UNLINK到全量清空与内存治理
那是一个平平无奇的周三晚上21点37分我手机上的报警群突然炸了线上Redis实例内存使用率连续五分钟超过85%紧接着业务侧开始反馈接口超时。登录服务器一看used_memory已经涨到快6GB而maxmemory只有8GB照这个速度凌晨就会写满。那一晚我花了一个多小时做Redis清理缓存的工作——查Key、统计前缀、批量删除、降内存——事后复盘才发现大部分时间不是花在删除动作上而是花在理清哪些缓存该删、怎么删才不阻塞、删完会不会出更大乱子这三个问题上。这篇文章就把那一次和之后多次实践里关于Redis缓存清理的完整经验写下来从最基本的删除命令讲起到生产环境的全量清空策略再到如何从源头避免缓存堆积。适合正在维护Redis实例、或者第一次接到内存告警的朋友看完应该能少走我走过的弯路。1. Redis缓存为什么会膨胀成垃圾场1.1 没有TTL的Key是头号元凶很多团队第一次处理Redis内存告警第一反应是是不是流量太大、数据太多。但我去看过几个项目之后结论往往很一致最大的一堆缓存Key是根本没有设置过期时间的。比如一个简单的登录场景代码里写了SET login:token:user123 xxxx用来存登录态。结果开发忘记在下一行补EXPIRE这个Key就永远活着。用户名、手机号、昵称这些数据被缓存起来之后如果业务逻辑没有写TTL它们会越堆越多一个月、三个月、半年内存一点点吃满。我在一次排查中统计过一个运行了大半年的服务Redis实例里Key的总数是1800万个其中900万个没有TTL占了内存的大头。这900万个Key里面有相当一部分是活动入口的配置、用户历史行为、临时计算结果的投影每次访问如果找不到就重新写一份写进去又从不清理最后就成了内存里的僵尸数据。所以在讨论清理之前我建议先给实例里的Key做一次体检用INFO keyspace看总Key数再用MEMORY USAGE user:info:12345抽查几个大Key或者用redis-cli配合SCAN统计一下没有过期时间的Key占比。这一步决定了接下来的清理方案——如果无TTL的Key很多清理动作就必须重一点如果都设置了合理的过期时间但内存还是高那重点就是查大Key和内存碎片。1.2 过期Key并不会到点就消失还有一个容易误解的点Redis里设置了TTL的Key不一定会在你期望的时间点立刻被删掉。网上很多文章说Redis过期Key由惰性删除和定期删除两个策略共同管理这句话是真的但很多人没注意它的潜台词过期Key的实际消失时间是不确定的。惰性删除的意思是当客户端访问一个Key时Redis会检查它是否已过期如果过期就删除并返回空。定期删除的意思是Redis每隔一段时间会随机抽一批设置了过期时间的Key把其中过期的删掉。这两个机制结合起来能处理大部分过期Key但问题在于如果一个Key过了TTL之后再也没有被访问同时定期删除又没扫到它它就会一直留在内存里直到哪一次随机扫描碰巧选中它。这个特性我称之为过期Key的退休延迟。所以你会发现即使所有Key都设置了TTLRedis的内存曲线仍然是锯齿形的——每到定期删除扫过一批内存掉一块然后又被新写入填上。处理这种问题不能只靠TTL自愈需要主动扫描一次已经过期但还在内存里的Key或者干脆用UNLINK把这些占位清掉。1.3 大Key和冷数据在拖后腿第三个被低估的问题是大Key。大Key没有一个绝对标准通常是字符串Value超过几十KB或者一个哈希、列表、集合里塞了几万甚至几十万个元素。这种Key内存占用巨大更麻烦的是操作代价极高。比如一个用户购物车如果实现成哈希并且把该用户所有的历史商品都塞进去这个Key可能膨胀到几十MB。平时访问没问题但一旦业务要清理它、或者它过期需要删除Redis在释放这几十MB内存时会同步阻塞一次操作可能卡住数百毫秒甚至数秒。对线上服务来说这就是妥妥的事故。冷数据则是另一类问题。有些数据写入后业务上已经不再访问了但因为你没有给它设定合理的TTL或者TTL设得太长它就一直在内存里躺着。冷数据不清理最直接的影响就是内存利用率低热数据还没有地方放过早触发了淘汰策略把真正有价值的Key提前赶出去。2. 最基础的清理操作DEL与UNLINK怎么选2.1 DEL的阻塞陷阱确定了哪些Key要删很多人第一反应是使用DEL命令redis-cli DEL user:info:12345这个命令在小Key场景很爽快但遇到大Key就要小心了。DEL删除一个大的列表或哈希时Redis必须同步遍历这个Key的所有元素并释放内存。我在一次清理任务中运行了DEL一个约50万成员的SetRedis主线程直接卡了将近3秒那3秒里线上所有读写全部排队接口平均耗时从20ms飙到了2秒以上。所以我的习惯是删除前先用OBJECT ENCODING和STRLEN、LLEN、SCARD、HLLEN这些命令看一眼Key的类型和规模如果是一个明显的大Key就坚决不用DEL而是改用另一个命令。2.2 UNLINK异步删除的正确姿势Redis 4.0开始提供了UNLINK语义上和DEL完全一致把Key从键空间里移除之后客户端再读这个Key就会返回空。区别在于UNLINK的移除键空间和释放内存是拆开的——它先把Key从主字典中摘除然后把真正回收内存的活儿丢给后台线程。redis-cli UNLINK user:info:12345删除大Key时UNLINK几乎不阻塞主线程这是它最大的价值。但要注意一个滞后感执行UNLINK后内存不会立刻下降因为后台线程还在慢慢释放内存。我遇到过好几次命令返回了1团队群里大家欢呼半天结果used_memory等了十几秒才明显往下掉有人还误以为删错了。这不是BUG是异步删除的正常现象。另一个细节是UNLINK并非对所有场景都异步。如果Key非常小Redis为了效率反而会直接在主线程释放效果等同DEL。这个细节不需要过度关注但解释了为什么UNLINK删小Key也很快。2.3 删除Key不是越多越好实际操作中我见过有人写了一个脚本把匹配某个前缀的Key全部删除结果把一份需要长期保留的基础数据也删了最终数据库被击穿爬起来花了两小时才恢复。所以清理前建议先回答三个问题这些Key在业务中还有没有引用删掉之后是否会重新生成、生成的数据和原来是否一致如果删出问题回滚方案是什么对于临时缓存类数据删除后可以重建风险低对于幂等结果、计数器、分布式锁这类状态型数据直接删除就可能导致后续业务判断出错。分布式锁就是典型如果缓存里的锁记录被当成普通缓存清掉另外的线程可能拿到同一个锁导致并发安全问题。这也是很多团队在清理缓存时特意排除掉锁前缀的原因。3. FLUSHDB与FLUSHALL全量清空是一把双刃剑3.1 两条命令的准确语义谈到最危险但也最彻底的操作全量清空不可不提。Redis提供了两条命令FLUSHDB清空当前数据库FLUSHALL清空整个实例的所有数据库。# 清空当前db redis-cli FLUSHDB # 清空整个实例 redis-cli FLUSHALL # Redis 4.0之后还可以带上ASYNC参数异步清空 redis-cli FLUSHALL ASYNC默认情况下FLUSHDB和FLUSHALL是同步执行的这意味着如果有海量Key命令执行期间主线程会被阻塞。Redis 4.0之后支持ASYNC参数生产环境推荐用异步方式原理和UNLINK类似先把键空间释放内存交给后台慢慢回收。另外提醒一句注意DB编号别搞错。一个实例开了多个DB的话FLUSHDB只清当前选中的库FLUSHALL是全库无差别清空。我见过有人在DB0上执行FLUSHDB结果以为清的是DB3排查半天才发现搞错了库。3.2 适用场景测试环境与可重演数据我在什么场景下会毫不犹豫用FLUSHALL本地开发库、测试环境、预发布环境的Redis。这些环境的数据本来就是造出来的丢了不影响线上反而能让开发拿到一个干净的内存状态。在压测前清空一次缓存也能让性能数据更客观——不然跑到一半缓存的命中率忽高忽低不好判断瓶颈。生产环境呢除非是明确的缓存重建窗口、而且全量数据都能从数据库或其他数据源恢复否则我绝不动FLUSHALL。哪怕业务方拍着胸脯说全都是缓存丢了没事我也要先确认万一丢完之后有请求路径从Redis里读到空然后往数据库写回错误数据这个风险谁来兜底3.3 我踩过的一次FLUSHALL的坑这里必须分享一次真实事故。某次版本升级运维计划把Redis缓存全部清掉让用户重新登录、重新拉取配置。命令执行得很顺利FLUSHALL ASYNC一下就返回了内存也开始下降。结果五分钟内数据库主库的CPU被打到90%慢查询爆满一批写接口开始超时。原因很简单这个Redis不只是缓存里面还存着一些半持久化的会话数据。很多服务启动时依赖这些会话判断用户状态缓存一清空所有请求同时穿透到数据库去重建会话数据库瞬间成为单点。那次事故持续了大概40分钟最后重启了部分服务、拦截了非核心流量才缓过来。所以现在我的生产环境全量清空清单是这样的先确认Redis里没有半持久化数据然后确认所有Key都能从上游重建接着在低峰期执行最后监控数据库连接数和慢查询至少30分钟。顺序不能变任何一个环节没确认都别碰FLUSHALL。4. 按Key模式精细化清理SCAN、类型差异与可视化工具4.1 用SCANMATCH做前缀清理生产环境往往不需要全量清空只需要清理某个业务前缀。这时最容易犯的错误是用KEYS命令# 不要在生产环境长时间跑KEYS redis-cli KEYS user:*KEYS会遍历整个键空间一次性把所有匹配的Key返回当Key数量达到百万级主线程会被拖死。正确做法是用SCAN它每次只返回一小批Key并给一个游标客户端拿着游标继续迭代直到游标回到0才表示遍历结束。redis-cli --scan --pattern user:* | head -100如果要用脚本批量删除可以参考下面这个思路按每次1000个Key的节奏边删边观察内存和耗时#!/bin/bash cursor0 while true; do result$(redis-cli SCAN $cursor COUNT 1000) cursor$(echo $result | head -1) keys$(echo $result | tail -n 2) if [ -n $keys ]; then echo $keys | xargs -n 100 redis-cli DEL fi if [ $cursor -eq 0 ]; then break fi done这里有个关键点SCAN不是一次到位的它返回的游标和数据分布会受并发写的影响所以即使游标走完也可能存在少量漏网之鱼。如果要求彻底清理建议多跑一两轮或者把MATCH前缀的Key在业务低峰期再检查一遍。4.2 不同数据类型的清理差异很多清理任务不是简单DEL一个Key而是只想清掉某个Key内部的一部分数据。比如用户购物车哈希里挂着几条历史记录你不能整个删除用户维度而要用HDEL只删部分字段。数据类型不同能做到的粒度差异很大整理一张表方便参考数据类型清理命令说明StringDEL / UNLINK直接删整个Key哈希 HashHDEL key field只删指定字段Key保留列表 ListLTRIM key start stop裁剪区间保留想要的段集合 SetSREM key member删除指定成员有序集合 ZSetZREM key member按成员删除也支持按score批量ZREMRANGEBYSCORE流 StreamXDEL key id按消息ID删除或XTRIM裁剪如果清理脚本里同时涉及多种类型我建议先判断类型再选命令避免一个DEL把所有东西都删了。尤其是哈希和列表这种一个Key里多段数据的类型看到DEL就全没了后续业务重建成本很高。4.3 可视化工具便捷但要有边界意识除了命令行我用得比较多的是Another Redis Desktop Manager简称ARDM这一类可视化客户端。搜索Key、看内存占用、按前缀筛选都非常直观也比手敲命令安全一点——至少能选中具体Key再删不会误伤其他前缀。不过可视化工具底层调用的还是DEL和UNLINK大Key的阻塞风险并不会因为界面化就消失。我曾经在界面里勾选了一千个Key批量删除结果里面有几个字符串还好某一个超大List让操作卡住了。界面工具爽快但删除前还是要对每个Key的大小有概念。还有一种场景是清理过期但未删除的Key。在ARDM里可以按TTL排序把TTL为-1永不过期的Key筛出来逐个看看是不是该删。这个排序功能比命令行直观得多我每次巡检内存时都会用。5. 生产环境清理缓存的完整流程与避坑清单5.1 动手前先确认三张保命清单真正到了生产环境我不会一上来就敲删除命令而是先确认三张清单。第一张是备份清单Redis实例是否有RDB或AOF持久化如果清理后出了问题能恢复到清理前的状态吗很多Redis作为纯缓存使用时没开持久化这种情况我会把清理范围内的Key先导出一份清单至少知道删了哪些回滚时能按清单重建一部分。第二张是监控清单内存使用率、命中率、数据库慢查询、业务接口错误率这四个指标在清理前后都要盯住。我习惯把Prometheus的页面开着或者用redis-cli INFO stats每秒刷一刷心里有数。第三张是回滚清单确认删除后如果出事是能通过重启服务恢复还是能从数据库重建或者有备份可以直接恢复。没有回滚方案的清理动作本质上是一次赌博。5.2 执行时节奏比速度重要清理缓存是一件慢就是快的事。宁愿分批次跑也不要一个脚本把几百万Key全干掉因为一旦方向错了批量删除会瞬间放大错误。我的参考流程是先用SCAN统计目标Key的存量然后用UNLINK按前缀分批删除每批1000个Key左右批与批之间sleep 1到3秒观察Redis主线程耗时和内存变化如果内存稳定下降、业务指标正常再加速一旦发现某个批次引起慢查询或者数据库压力升高立刻暂停而不是硬跑到底。redis-cli --scan --pattern session:* | while read key; do redis-cli UNLINK $key sleep 0.5 done这个脚本看起来笨但胜在每条命令之间有时间间隔Redis不会被瞬间压垮。如果Key数量实在太大还可以配合Lua脚本减少RTT但前提是你对Lua脚本的执行语义很熟否则容易把原本的异步操作变成一轮阻塞。5.3 清理后盯着内存和数据库看30分钟删除命令返回成功不代表战斗结束。我通常会在清理后继续观察三个点。一是used_memory是否真的降下来了。如果用UNLINK或FLUSHALL ASYNC内存下降是滞后的别急着下结论等10到15分钟再看。如果内存降幅远低于预期考虑是不是还有一批Key没删干净或者Redis内部的jemalloc没有把内存归还给操作系统——这种情况可以尝试MEMORY PURGE但需要明确它只是让分配器把空闲内存归还给OS不保证每次都有明显效果。二是数据库负载。缓存清理掉之后大量请求会穿透到MySQL或下游服务慢查询数量和数据库CPU会在短时间内上升。如果这个上升幅度在可控范围说明Key确实该清理如果直接打满说明你的缓存承载量比想象中大清理窗口选错了。三是业务功能抽查。找一个使用这些缓存Key的核心链路实际跑一遍确认返回数据正确。别只盯着指标看真实业务验证才是最靠谱的验证。6. 从临时清理走向缓存治理6.1 TTL设计给每个Key一条生命线清理缓存这件事做得再熟练也是被动的。真正让我告别深夜救火的是后面把这些经验沉淀成了缓存治理规范。第一条规范就是TTL。写进Redis的Key默认必须带上过期时间除非有明确理由它需要永久存在。这个默认值按业务类型设计验证码、短信码五分钟登录态和Token两小时以内热点榜单和配置一小时左右可以重算的聚合数据半小时到两小时不等。定完这些规则后在代码评审里多一道检查——凡是SET成功了却没有EXPIRE的都要打回。有人担心TTL太短会导致缓存命中率下降实际上这个担心可以在上线后用数据验证。一个Key如果频繁被重建说明它的TTL小于两次访问间隔可以适当调长如果久久没被访问那它的存在本身就存疑。TTL不是拍脑袋定的而是跟着访问规律走。6.2 内存淘汰策略给Redis一个兜底方案不管TTL设计得多好总会有突发流量把内存写满。这时maxmemory和maxmemory-policy就是第二道防线。策略行为适用场景noeviction内存写满时不淘汰直接返回OOM错误不能丢数据的场景但要小心写失败allkeys-lru从所有Key中按LRU淘汰最久未用的纯缓存场景性价比高volatile-lru只从设置了TTL的Key中按LRU淘汰有部分Key希望永远保留allkeys-random从所有Key中随机淘汰访问模式均匀、无明确热点volatile-random只从设置了TTL的Key中随机淘汰适合混合数据volatile-ttl优先淘汰剩余TTL最短的Key适合能预判过期顺序的场景我在大多数缓存场景里推荐allkeys-lru因为它的淘汰依据是真实访问热度如果Redis里混着一些必须保留的配置型数据那就用volatile-lru并保证那些永久Key不设TTL。注意分配策略生效的前提是maxmemory有配置很多人漏了这一步Redis就可以无限写满直到宕机。6.3 Key命名规范与定期巡检最后一条规范是Key命名。我见过很多缓存垃圾场根本原因是Key名没有统一规则清理脚本无从下手。建议按业务域:模块:用途:标识的格式命名比如order:detail:123456、session:user:789这样无论人工排查还是脚本按前缀清理都能很快圈定范围。配合命名规范每月甚至每周做一次巡检用INFO和SCAN统计各前缀Key数量、内存占比、无TTL比例把最大的一批Key列出来。这个巡检习惯坚持三个月你会发现清理工作越来越轻因为问题还没变大就被发现了。我自己现在给团队定了一个最简单的目标Redis实例里的每个Key都应该能说清楚它从哪来、活多久、死了怎么办。当这三个问题都能回答时清理缓存就不再是噩梦了。写到这里顺带说点掏心窝的话。我经历过太多次缓存又爆了的凌晨一开始觉得是运维问题后来觉得是开发问题最后才想明白清理缓存根本上是设计问题。Redis是一个内存资源极度有限的空间你往里面放什么、放多久、占多大地方都应该有意识地去控制。每一次需要手动大扫除都是平时设计欠下的债。如果这篇内容能给你留一个习惯我建议是每次上线前把缓存自检清单作为发布检查的一环——确认新Key有没有TTL、命名是否规范、会不会无限增长。这套动作三分钟就够但值得你省下未来三个通宵。这是我个人实操中最值钱的一条经验。如果你也在维护Redis实例祝你的Key永远干干净净内存曲线永远平坦。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →