Redis key数量失控?Shell脚本组合拳实现高效清理与长效治理 去年秋天我接到一单线上求助某个 Redis 集群的内存其实还在可控范围但是INFO keyspace里单节点 key 数量已经超过 1.2 亿。业务方一开始想扩容我去看了一眼接入方式发现问题的核心根本不在于内存而在于 key 数量失控。后来我用一套 Shell 脚本完成了治理把僵死 key 数量压到原来的三分之一以下整个过程没有停服也没有改一行业务代码。这套方案就是后来沉淀下来的 DeepSeek一个用 Shell 脚本 redis-cli 的组合拳专门用来降低 Redis 中 key 数量的完整方案。如果你也在维护一个 key 数量疯狂增长的 Redis 实例这篇文章应该能帮你省下不少脑细胞。1. key数量失控的根源和成本动手前先看清楚1.1 三个最常见的失控来源先说失控的原因。我见过的大量 key 增长基本逃不出下面这三种情况。不带过期时间的缓存。这是最常见的。很多业务同学写缓存的时候只记得SET不记得EXPIRE甚至有的框架默认就不设置 TTL。这些 key 一旦写入就永远存在时间一长就成了僵尸数据。比如你把用户的购物车信息写进 Redis只做了SET cart:12345 ...一个月后这批数据还在内存里可用户早就不看了。动态维度 key 无限膨胀。第二种是 key 设计里带了动态维度比如按订单号、按 requestId、按日期拼 key。订单号本身是无限增长的那么order:{id}:detail这种 key 也会无限增长。哪怕每个 key 只有几百字节一天一千万订单一年就是几十亿个 key。缓存穿透兜底造成的空值缓存。还有一种是做缓存穿透保护时把不存在的数据也缓存起来了。比如数据库里查不到某个用户就写一个empty空值进 RedisTTL 设置很短看起来没什么问题。但如果攻击流量很大、或者业务场景比较极端这些空值 key 也会在短时间内堆积成一个天文数字。1.2 成本维度不只是内存这才是最要命的很多人以为 Redis 里 key 多了只是费内存其实远不止。持久化文件膨胀RDB 和 AOF 里要记录大量 key备份文件变大恢复时间变长。fork 阻塞风险Redis 做 RDB 持久化时fork 子进程要复制父进程内存页表key 数量越多内存越大fork 阶段阻塞主线程的风险越高。主从全量同步变慢从库重新全量同步时要把 RDB 传到从库key 越多传输时间越长同步窗口越大。过期键扫描开销Redis 的过期 key 清理依赖于定时任务和惰性删除key 总量过大时每次扫描的耗时也会上升。内存碎片大量小 key 频繁写入删除会让内存碎片率升高最终实际占用比业务数据大得多。我把这些影响整理成了一张表方便你跟业务方解释为什么要治理 key 数量成本维度具体影响持久化与备份RDB/AOF 文件膨胀备份和恢复时间成倍增加fork 阻塞内存占用过高时fork 可能导致主线程卡顿主从同步全量同步 RDB 文件变大从库恢复时间拉长过期扫描每次周期扫描的对象变多CPU 消耗上升运维排障bigkeys、scan、内存分析等操作耗时变长1.3 先算一笔账再决定要不要动手我之前处理过一个支付相关的系统每天新增订单 key 大概 80 万平均每个 key 加 value 约 400 字节。听起来不大但如果不设置过期时间一个月就是 2400 万 key、大约 10GB 数据一年就是接近 3 亿 key、120GB 内存。关键是这些 key 里大部分在写入第二天就没有任何访问了。所以判断要不要治理不要只看内存水位还要看 key 增长率、命中率、活跃 key 占比这些指标。如果keyspace_hits远小于keyspace_misses或者扫描后发现大量前缀相同的 key 已经长时间没有访问那就该动手了。2. 降低key数量的主流路径我为什么最终选择Shell脚本组合2.1 业界常见的四条路面对 key 数量膨胀大致有四条路可以走。代码层治理。改业务代码统一缓存封装默认设置 TTL规范 key 命名从源头杜绝问题。这是根治方案但缺点是周期长而且存量数据还是得有人清理。适合作为长期方案推进。内存淘汰策略。给 Redis 配置maxmemory-policy allkeys-lru或者volatile-ttl之类的淘汰策略让 Redis 自己想办法清理。说实话这是最后一道保险风险在于可能误删还在使用的 key因为 LRU 只是近似而且如果全是无 TTL 的 keyvolatile-ttl根本不起作用。我一般只建议作为兜底不作为主动治理手段。冷热数据分离。把一部分冷数据从 Redis 迁移到其他存储比如磁盘型 KV 存储或者对象存储只留热数据在 Redis。这条路对架构改动较大适合长期演进不适合应急止血。脚本批处理。写脚本定期扫描、统计、删除或者设置过期时间。见效快不用改业务代码风险可控。缺点是需要有人对数据语义足够清楚防止误删。这是短期止血、中期辅助的首选。2.2 方案对比说实话脚本方案最灵活我习惯用一张表来做决策对比方案见效速度风险适用场景代码层治理TTL 规范慢低长期根治内存淘汰策略快中高紧急兜底冷热数据分离慢中数据量极大且长期增长Shell 脚本批处理快中可控存量治理、日常巡检实际项目里我一般会把代码层治理和脚本批处理一起上。脚本先处理掉存量脏数据代码层规范后续增量数据两边同时推进。2.3 为什么是Shell而不是Python或Go可能有人会说清理 key 这么复杂的事情用 Python 或者 Go 写不是更优雅一点吗我在做 DeepSeek 方案时倒没有纠结太久原因很现实。redis-cli 本身已经具备核心能力。扫描用--scan批量执行用--pipe统计用INFO很多时候根本不需要额外的 SDK。部署环境里一般都有 Shell。运维服务器、堡垒机上不一定有 Python 环境和 Redis SDK但基本都有 Bash 和 redis-cli。配合 crontab 非常自然。Shell 脚本天然适合挂在定时任务里生成日志、错误退出、邮件告警都容易处理。另外 Shell 脚本方便审计和解释。你要给业务方看你到底跑了什么命令直接贴脚本比贴 Python 类更直观。对临时治理任务来说简单粗暴绝对要比花哨重要。3. 脚本核心设计与实现从SCAN到分批删除的完整链路3.1 红线不要用 KEYS请用 SCAN不管你是统计还是清理第一条铁律就是不要在生产环境用KEYS命令。KEYS会一次性遍历整个 keyspace而 Redis 是单线程模型KEYS在 key 数量大的实例上执行几秒甚至几十秒期间所有读写都会被阻塞。生产事故就是这么来的。正确做法是用SCAN命令配合游标迭代。SCAN每次返回少量 key不会阻塞主线程。redis-cli 给我们封装好了redis-cli --scan --pattern temp:*这条命令会持续迭代扫描把匹配temp:*的 key 输出到标准输出每行一个 key。它是流式输出的不会一次性加载到内存所以即使有上千万 key 也能处理。3.2 第一步先摸清分布统计各前缀的 key 数量动手清理之前必须先搞清楚哪些前缀的 key 占了大头。我通常会写一个简单的统计脚本#!/bin/bash # 统计指定 pattern 下 key 数量 for pattern in user:* order:* cart:* temp:* session:*; do count$(redis-cli --scan --pattern $pattern | wc -l) echo -e $pattern\t$count done不过这里有个坑如果某个前缀下的 key 数量特别大这个脚本会跑很久因为它等于全量扫描了一遍。所以更稳妥的做法是先抽样评估或者用--count参数控制每次 SCAN 的步长。3.3 核心删除逻辑分批管道避免一把梭确认了要清理的前缀之后就到了核心删除环节。最直接的方式是redis-cli --scan --pattern temp:* | xargs -n 1000 redis-cli DEL这个命令会把匹配到的 key 每 1000 个作为一组调用一次redis-cli DEL。优点是简单缺点是大量 key 时仍然比较粗暴而且不能很好的限速。我在 DeepSeek 方案里用的是更加可控的分批管道模式先把 key 列表落盘然后按批次执行DEL每批之间 sleep 一下给 Redis 喘息的时间。#!/bin/bash # 批量删除 Redis key支持分批与限速 # 用法: ./redis_clean.sh temp:* 1000 2 pattern${1:-temp:*} batch_size${2:-1000} sleep_seconds${3:-2} key_file/tmp/redis_keys_$(date %s).txt pipe_file/tmp/redis_del_pipe.txt log_file/tmp/redis_clean.log # 1. 导出 key 列表到本地文件 redis-cli --scan --pattern $pattern $key_file total$(wc -l $key_file) echo $(date %F %T) 待处理 key 数量: $total | tee -a $log_file # 2. 分批写入管道文件并执行 count0 : $pipe_file while IFS read -r key; do printf DEL %s\r\n $key $pipe_file count$((count 1)) if [ $((count % batch_size)) -eq 0 ]; then echo $(date %F %T) 已累计 $count 条开始批量删除... | tee -a $log_file redis-cli --pipe $pipe_file $log_file 21 : $pipe_file sleep $sleep_seconds fi done $key_file # 3. 处理剩余不足一批的数据 if [ -s $pipe_file ]; then redis-cli --pipe $pipe_file $log_file 21 fi echo $(date %F %T) 处理完成共处理 $count 个 key。 | tee -a $log_file这个脚本解决了三个问题第一key 列表先落盘后续批量处理如果断了不用重新扫描第二使用redis-cli --pipe走 RESP 协议管道批量发送DEL命令效率比逐条执行高很多第三每批处理完固定 sleep 几秒避免瞬时压力过大。注意redis-cli --pipe需要 Redis 2.6.14 以上版本才支持现在的版本基本都不用担心。3.4 不是所有 key 都该立刻删除设置过期时间反而更稳实际治理过程中你会发现有些 key 你拿不准是否还在使用。这时候直接删除有风险更好的做法是给它设置一个较短的过期时间让 Redis 在 TTL 到来后自动清理。这样做的好处是如果业务还在用最多就是一次缓存穿透应用会重新回源填充如果已经没人用了到期后自然消失不会产生误删事故。给 key 设置随机 TTL 也可以防止雪崩#!/bin/bash # 给匹配到的 key 设置随机过期时间范围 3600~7200 秒 patterntemp:* while IFS read -r key; do ttl$((RANDOM * 3600 / 32767 3600)) printf EXPIRE %s %d\r\n $key $ttl done (redis-cli --scan --pattern $pattern) | redis-cli --pipe /dev/null这里我用RANDOM生成了一个 3600 到 7200 秒之间的随机 TTL。为什么要随机如果所有 key 都在同一个时间点过期很可能给下游数据库造成一波瞬时回源压力也就是常说的缓存雪崩。随机 TTL 能把压力摊开。3.5 特殊字符与空格 key 的隐藏陷阱这个坑我踩过一次。key 名称里如果带了空格比如cart:2024:01 01直接用xargs会被拆成两个参数删除的时候不仅删不掉还可能误删别的 key。安全做法是用--null参数让 redis-cli 输出以空字符结尾的 key 列表然后用xargs -0来处理redis-cli --scan --pattern cart:* --null | xargs -0 -n 1000 redis-cli DEL同样的在while read循环里我建议显式指定IFS read -r key避免默认的 IFS 把 key 头尾的空格和制表符吞掉。4. 上线前必须做好的三件事预演、限速与止损预案4.1 先在预发或只读副本上跑通统计流程我强烈建议不要一上来就在生产主实例上执行大面积删除。第一步先在预发环境执行统计脚本确认你要清理的 pattern 完全匹配预期不会误伤核心业务数据。如果没有预发环境可以在生产环境先拿一个流量很低的只读从库做验证观察SCAN的输出结果。如果是从库redis-cli 可以连接从库并执行READONLY模式扫描这样不会影响主库性能。需要注意的是从库可能有一些延迟统计的 key 列表跟主库不是完全一致但作为预演足够了。4.2 用并发和批大小控制施压速度清理脚本不要全速跑。Redis 虽然处理DEL很快但大批量删除时瞬时 QPS 还是会冲上去可能影响业务。我一般用下面这套参数来控制压力场景批大小批次间隔并发数低峰期清理50001s1高峰期小批清理10002~3s1紧急大规模清理100001s2~4DESI模式下我一般不推荐开并发因为 Redis 单线程模型下并发redis-cli反而可能增加瞬时命令排队性能提升有限。如果要加速优先加大批大小而不是提高并发。另外清理过程中要持续观察INFO stats里的instantaneous_ops_per_sec和slowlog如果波动明显需要及时调低批大小。4.3 删除前的备份、白名单与止损预案任何删除操作都要有后悔药。我做的第一件事是把 key 列表导出并 gzip 备份redis-cli --scan --pattern temp:* | gzip /tmp/keys_backup_$(date %s).txt.gz这样即使误删了还能根据列表恢复。不过要说明恢复 key 的 value 需要额外的DUMP/RESTORE机制我这里备份的是 key 名单作用主要是分析、排查而不是完整的快照。真正的多级保护还是靠白名单。在清理脚本里我会增加一个白名单文件凡是匹配白名单的 key 直接跳过while IFS read -r key; do case $key in temp:important:*|temp:keep:*) echo 跳过白名单 key: $key continue ;; esac printf DEL %s\r\n $key done $key_file | redis-cli --pipe止损预案方面我的经验是清理期间安排一个熟悉业务的人盯群一旦业务方反馈缓存丢失立即停掉脚本。如果使用 TTL 方式而不是直接删除即使误操作也只导致缓存回源不影响数据正确性这也是为什么我前面强调不确定就先设置过期时间。5. 实测中的两个关键坑SCAN返回大Key和连接抖动5.1 大 key 才是真正的定时炸弹SCAN 本身不阻塞但如果你用SCAN扫到了一个大 key比如一个包含几十万个字段的 hash然后对它执行DELRedis 会一次性释放这个 key 占用的内存这个过程在单线程模型下同样会造成卡顿。我见过一个实例因为删了一个 200MB 的 hash key主线程卡了好几秒。遇到大 key正确的做法是渐进式清理而不是直接删除。比如对 hash 类型用HSCAN配合HDEL分批删除字段等 hash 里的字段清空了key 自然就没了。同样的逻辑适用于 set 用SSCAN配合SREM、zset 用ZSCAN配合ZREM。# 渐进式清理大 hash 示例每次删除 100 个 field redis-cli --scan --pattern bighash:* --null | xargs -0 -n 1 redis-cli --raw HSCAN $key 0 COUNT 100 | # 这里仅示意实际需要循环处理游标这个脚本我简写了核心思路就是不要用DEL直接处理大 key而是把内部元素分批删掉。5.2 redis-cli 连接超时和网络抖动大批量执行redis-cli命令时可能会遇到连接超时。尤其是通过堡垒机、跳板机连接 Redis 的场景长时间运行后连接被中途掐断非常常见。我在脚本里一般会加上连接超时和 TCP keepalive 参数redis-cli --connect-timeout 5 --tcp-keepalive 60 --scan --pattern temp:*--connect-timeout控制 TCP 连接建立的超时时间--tcp-keepalive会让 TCP 连接定期发送保活包避免长时间空闲被中间设备断开。另一个容易忽视的问题如果脚本在批量删除时中断了再次运行时不要重新扫描直接使用之前落盘的 key 文件避免重复扫描线上的高负载。我已经习惯把 key 列表导出来作为任务队列每处理完一个批次就在进度文件里记录处理的最后一条 key。下次中断后从进度文件对应的位置继续往下读就行。5.3 删除后内存没降下来别慌清理完大量 key 之后INFO memory里的used_memory可能不会立刻降下来。这有两个原因一是 Redis 释放的内存并不会马上归还给操作系统而是留在自己的内存分配器里复用二是内存碎片率高的时候即使逻辑数据少了物理内存占用依然偏高。如果确认删除了大量 key可以用MEMORY PURGE主动整理但这个命令在某些版本里会比较耗时建议低峰期谨慎执行。同时可以打开activedefrag让 Redis 在后台自动整理碎片。6. 治理之后别让key数量反弹6.1 建立 key 数量巡检脚本清理完成不代表结束要防止它反弹。我习惯写一个巡检脚本放到 crontab 里每天定时统计 key 总量和关键前缀的数量。比如每 10 分钟执行一次记录到日志文件#!/bin/bash # /usr/local/bin/redis_key_count.sh echo $(date %F %T) $(redis-cli dbsize) /var/log/redis_key_count.log如果想统计不同前缀的分布可以基于redis-cli --scan --count 1000做采样统计不用全量扫描。这样既能监控总量又能了解增长来源。6.2 代码层的长效措施脚本治理是治标代码层规范才能治本。从长期看建议业务侧做几件事所有缓存 key 默认设置 TTL即使是空值缓存也不能例外key 命名按业务模块分层方便识别接入统一的缓存 SDK在 SDK 层强制设置过期时间避免业务同学遗漏。我自己在推进的时候会先整理一份Redis Key 规范文档把命名规则、TTL 要求、风险评估表格发给大家。短期靠脚本清理长期靠规范约束。6.3 用可视化工具观察治理效果虽然我们这套方案是命令行为主但治理过程中确实还需要一个可视化工具来观察 key 的变化趋势。我在用的终端工具比你想象中简单Redis Desktop Manager 或 Another Redis Desktop Manager 都可以连接实例后直接看 key 数量、内存、TTL 分布配合脚本日志能更直观地判断清理效果。不过可视化工具我不建议直接在这种大批量清理场景里操作因为图形界面的批量操作能力远不如命令行灵活它更适合事后观察和验证。我个人的习惯是清理前截一张图清理后再截一张图对比给业务方看最直观。内存曲线的变化、key 总数的下降用数据说话。最后再分享一个我在实际治理中反复用到的小技巧清理操作前把目标 key 的列表导出来不管脚本多完美都要给自己留一条退路。尤其是那种业务迭代过快、key 语义已经模糊的实例宁可多花十分钟做备份也不要赌一把直接删。我在 DeepSeek 方案里始终坚持这个原则这也是它能平稳处理几亿 key 都没有出现事故的关键一条。