news 2026/9/13 15:44:30

Redis key数量失控?Shell脚本组合拳实现高效清理与长效治理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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空值进 Redis,TTL 设置很短,看起来没什么问题。但如果攻击流量很大、或者业务场景比较极端,这些空值 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 的 key,volatile-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. 分批写入管道文件并执行 count=0 : > "$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" 2>&1 : > "$pipe_file" sleep "$sleep_seconds" fi done < "$key_file" # 3. 处理剩余不足一批的数据 if [ -s "$pipe_file" ]; then redis-cli --pipe < "$pipe_file" >> "$log_file" 2>&1 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 秒 pattern="temp:*" 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~4

DESI模式下我一般不推荐开并发,因为 Redis 单线程模型下,并发redis-cli反而可能增加瞬时命令排队,性能提升有限。如果要加速,优先加大批大小,而不是提高并发。另外,清理过程中要持续观察INFO stats里的instantaneous_ops_per_secslowlog,如果波动明显,需要及时调低批大小。

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,然后对它执行DEL,Redis 会一次性释放这个 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 都没有出现事故的关键一条。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 15:43:19

Python实现地铁ACC数据分钟级客流预测系统

简介&#xff1a;本资源是一套基于Python开发的地铁客流预测系统完整实现&#xff0c;面向城市轨道交通数据分析初学者、交通工程专业学生及Python Web开发学习者&#xff0c;聚焦于利用ACC系统用户行程与站点数据开展线路级与站点级客流建模、预测与可视化预警。系统采用B/S架…

作者头像 李华
网站建设 2026/9/13 15:41:52

AR硬件交互原型系统:从光学标定到失效兜底的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 15:40:57

Prompt as Code:工业级提示词基础设施实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 15:40:14

CLT层合板Python建模:正交各向异性与A/B/D矩阵计算

简介&#xff1a;本资源是一套专为复合材料层压板力学分析设计的Python类库pyPLY&#xff0c;面向航空航天、汽车及土木工程领域的工程师与高校研究人员&#xff0c;解决经典层压理论&#xff08;CLT&#xff09;建模中材料定义、叠层配置、应力应变计算与失效评估等核心问题。…

作者头像 李华
网站建设 2026/9/13 15:39:04

marimo 单元执行机制全解:反应式执行、静态分析与运行时配置

marimo 单元执行机制全解&#xff1a;反应式执行、静态分析与运行时配置 【免费下载链接】marimo A reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. A…

作者头像 李华
网站建设 2026/9/13 15:38:22

STM32CubeProgrammer安装与AI嵌入式烧录实战指南

1. 项目概述&#xff1a;为什么STM32CubeProgrammer是嵌入式AI编程落地的“最后一道闸门” 在嵌入式软件AI编程这条路上&#xff0c;我见过太多人卡在最后一步——代码写完了&#xff0c;模型量化好了&#xff0c;推理引擎也集成进去了&#xff0c;可烧录到板子上就是不运行&am…

作者头像 李华