做 Redis 运维和业务开发的这几年,“清理缓存”这个词我听了无数次,也亲手操作了无数次。很多朋友一上来就问“清理缓存是不是执行 FLUSHALL”,每次听到这种问题我都心头一紧:兄弟,这可能是你 Redis 实例最短命的操作方式。Redis 清理缓存远没有“一条命令清库”那么简单,它涉及数据类型、过期策略、内存淘汰机制、业务容忍度、运维窗口等多重因素。这篇文章把我实际踩坑、复盘、优化的一套完整方法整理出来,覆盖从“怎么判断哪些缓存能清”到“怎么优雅地批量清理”,再到“清理后如何防止再度占满”的完整链条,希望能给正在为 Redis 内存发愁的朋友一些参考。
这篇内容适合这几类人:刚接触 Redis、在项目里把 Redis 当缓存用的业务开发;负责 Redis 实例稳定性的运维或 SRE;以及那些被“缓存满了”“内存飙高”告警折磨、想系统化解决问题的技术人员。不用担心基础问题,我会把命令背后的原理、为什么这个操作危险、那个操作安全都掰开讲清楚。
1. 动手之前:先搞清楚你要清理的 Redis 里到底存了什么
1.1 缓存不等于临时数据,先做一次“资产盘点”
很多人把“清理缓存”想得太简单,觉得 Redis 里的数据都是可以随时丢的临时数据。实际上,同一个 Redis 实例里可能同时住着几种完全不同性质的“住户”:第一种是纯临时数据,比如验证码、短期热点资讯、用户未登录时的临时购物车,丢了影响很小,顶多让用户重新操作一次;第二种是“准持久化”的业务数据,比如分布式锁、秒杀库存预扣、接口幂等标记,这类数据一旦被误删,轻则业务异常,重则带来资金或订单层面的问题;第三种是真正不能丢的会话和配置数据,比如登录态 token、用户权限缓存、灰度开关配置,删了会让大量用户被迫重新登录,或者让系统瞬间回到“无配置”状态。
所以我强烈建议:在按下任何删除命令之前,先做一份 Redis 键空间的“资产盘点”。怎么盘?很简单,通过redis-cli登录实例后,先看DBSIZE了解键总量,再用INFO MEMORY看内存分布,尽量搞清楚每个业务前缀各占多少。常用的命令是redis-cli --bigkeys,这个命令会扫描整个实例,帮你找到占用内存最大的 key、元素最多的 key,虽然它不是精确统计,但用于“摸家底”已经足够了。
盘点之后,你要给每个 key 前缀打上标签:可删、可延迟删、绝对不删。这个环节决定了你后续清理操作的安全边界。别嫌麻烦,一次十几分钟的盘点,能避免你后面花几小时处理误删事故。
1.2 别忽略 Redis 实例的“外围信息”
除了知道 key 的类型和分布,你还需要了解几件事:这个实例是单机、哨兵还是集群模式;它有没有开启持久化(RDB/AOF);它是给一个业务用,还是多个业务共用;它的内存上限maxmemory设置了多少,淘汰策略是什么。这些信息在清理缓存的决策中非常关键。
举两个真实例子。有一次我接到一个清理任务,服务器内存报警,同事直接在主库上执行了FLUSHALL,结果这个实例开启了 AOF 持久化且未做 rewrite,重启后数据全部恢复,内存照样爆满,等于白清了一场。另一次是集群环境,有人直接对某个分片执行了FLUSHDB,结果大量 key 被重建后仍散落在原分片,内存压力并没有转移到其他节点,业务侧的缓存命中率却骤降。Redis 的清理从来不只是“删数据”这一个动作,还要结合持久化设置、集群分片规则、业务调用链一起评估。
2. 清单个 key 还是清一批 key,选对命令很关键
2.1 DEL 和 UNLINK,你以为的“删除”可能没那么简单
在 Redis 里删除单个 key,大家最熟悉的是DEL,但它有一个隐藏问题:如果 key 是一个包含几百万元素的大 hash、大 set 或者大 list,DEL会在主线程里同步执行,阻塞 Redis 服务。在实际生产环境里,一个操作阻塞几秒钟,对上层业务来说就是一次明显的卡顿甚至超时风暴。所以处理大 key 时请优先使用UNLINK命令。UNLINK做的事情和DEL在逻辑上等价,但它是异步的:主线程只负责把 key 从键空间中解除引用并返回,真正释放内存的操作交给后台线程慢慢处理。这样可以非常有效地避免长耗时阻塞。
还有一个小细节:UNLINK并不是所有场景都比DEL好。对于小 key,两者性能差不多,甚至DEL更直接;对于大 key,UNLINK的优势是压倒性的。我的经验是:凡是你不确定 key 有多大、元素数量级未知的场景,一律用UNLINK兜底,把阻塞风险降为零。
注意:Drill down 到这里很多朋友会问,“那
DEL是不是永远不要用?”不是,小 key、低频操作、允许毫秒级阻塞的场景,DEL完全没问题。关键是你要有“先判断 key 大小再决定用哪个命令”的意识。
2.2 为什么不能用 KEYS 做批量清理,SCAN 才是正解
很多人在 Redis 里搜一批满足条件的 key 时,第一反应是KEYS prefix:*。这个命令在本地开发环境跑起来很爽,一旦上了生产,碰到几百万甚至上千万个 key,Redis 的单线程模型会被瞬间打爆。因为KEYS会在主线程里遍历整个字典,期间所有读写命令都排队等待,轻则接口超时,重则触发哨兵误判主节点宕机而切换。我见过不止一次因为执行KEYS导致 Redis 假死、Redis 集群节点间同步延迟飙升的事故。
正确的批量遍历姿势是SCAN。它跟KEYS最大的不同在于:SCAN是游标式的增量遍历,每次只返回一小部分 key(默认大概十个左右),不会一次性把所有 key 都加载到内存,也不会长时间阻塞主线程。你可以通过COUNT参数调整每次遍历返回的数量,但要注意COUNT只是提示值,不是精确限制。实际使用中我会写一个循环脚本,用SCAN配合MATCH模式过滤前缀,然后把拿到的 key 逐个或批量删除。
下面这段是很多项目里会直接用到的清理脚本思路(命令行到 Redis 的版本):
redis-cli --scan --pattern "user:session:*" | head -n 1000 | xargs -L 100 redis-cli UNLINK这段命令先用--scan模式匹配出前缀为user:session:*的 key,再分批用UNLINK删除。不过这里有两个坑要提醒:一是head -n 1000只取了前 1000 个 key,如果你的目标是清空所有该前缀的 key,需要去掉 head 限制;二是xargs -L 100表示每 100 个 key 执行一次UNLINK,一次性传入过多 key 会导致命令行过长,也会让 redis-server 一次性处理过多命令而效率下降。
2.3 不同数据类型的“精准清理”姿势
字符串类型最简单,UNLINK key或DEL key直接删就行。麻烦的是其他数据结构,因为你可能只想删掉一部分元素,而不是整个 key。比如一个大 hash 存储了某个用户群体的详细字段,你想清理其中某几个字段,就得用HDEL key field1 field2精准移除;如果你不知道有哪些字段,需要先用HSCAN分批获取字段名再做删除操作,不要用HGETALL,那同样会把所有字段一次性拉到内存里。
对于 set 类型,删除单个成员用SREM key member;如果你是按分数区间清理,比如只删除分数小于某个阈值的成员,就要用ZREMRANGEBYSCORE key min max。List 类型相对特别一点,它按索引存数据,如果想清理一部分元素,可以用LTRIM key start stop保留指定区间,区间之外的元素会被删除。这里有个典型误操作:很多人在不知道 list 长度的情况下执行LTRIM key 0 100,这不是“保留前 100 个元素”吗?错,LTRIM的 start 和 stop 都是包含两端下标的,如果你只想保留前 100 个,正确写法是LTRIM key 0 99。
不同数据类型的清理,本质上是一个“理解数据结构语义”的过程。你要清楚自己到底想清掉什么,再选择对应的删除命令。整个 hash 都要清就直接删除 key,只清一部分就用HDEL,这两者差别很大,用错了可能把不该删的业务数据也带走了。
3. 让 Redis 自己“自动清理”才是长久之计
3.1 过期策略:你设了 TTL,Redis 不一定立刻删
很多人以为 key 设置了过期时间,到了时间就会被立刻删除,内存马上释放。实际上 Redis 的过期键清理有两种机制:惰性删除和主动删除。惰性删除很简单——当你访问一个已经过期的 key 时,Redis 发现它过期了,就把它删除并返回空;但如果这个 key 过期后一直没人访问,它就会继续占着内存,这就是所谓“过期了还在占地方”的情况。主动删除则是 Redis 后台会定期抽样检查一部分设置了过期时间的 key,把其中过期的清理掉。
这两种机制配合起来,理论上能保证过期 key 最终被清掉,但在 key 数量极大、过期时间集中的场景下,你会发现一个现象:大量 key 到达过期时间后,内存并不会立刻掉下来,而是要等一段时间才慢慢下降。这不是 Redis 出 bug 了,而是主动删除的检查频率和抽查数量都是有限制的,它不想因为清扫过期 key 而影响正常的读写性能。所以如果你遇到“明明设置了过期时间,内存还是高”,先别急着骂,算一下是不是过期时间太集中、 key 数量太多的原因。
3.2 内存淘汰策略:当内存真的满了怎么办
当 Redis 内存达到maxmemory限制后,它会根据配置的淘汰策略来腾出空间。常见的有noeviction(不淘汰,直接报错给写命令)、allkeys-lru(从所有 key 里按 LRU 算法淘汰最近最少使用的)、volatile-lru(只从设置了过期的 key 里按 LRU 淘汰)、allkeys-random、volatile-ttl(淘汰剩余过期时间最短的)等。下面是几种常用策略的横向对比:
| 淘汰策略 | 作用范围 | 适用场景 | 风险 |
|---|---|---|---|
| noeviction | 无 | 允许写失败、不想丢任何数据的场景 | 写满后新写入直接报错 |
| allkeys-lru | 所有 key | 大多数缓存场景,最常用 | 可能淘汰掉还没过期但很少访问的“准持久化” key |
| volatile-lru | 设了 TTL 的 key | 只有部分 key 设了过期时间的场景 | 长期不设过期的 key 会导致内存无法释放 |
| allkeys-random | 所有 key | 访问分布非常均匀,无热点 | 热点 key 也可能被随机淘汰 |
| volatile-ttl | 设了 TTL 的 key | 优先淘汰即将过期的 key | 如果大部分 key 都有较长 TTL,效果不明显 |
这里我要特别劝一句:内存淘汰策略不是“自动清理缓存”的万能药。它只是在 Redis 达到内存上限时的一种兜底机制,不该被当成日常清缓存的工具。为什么?因为淘汰策略是全局生效的,它不会区分这个 key 是不是重要业务数据,只会死板地按算法选择牺牲品。你可能会发现,一些核心业务的热点数据因为访问模式变化被淘汰了,结果下游存储被打爆,业务出现大面积超时。真正合理的做法是:给每个 key 设计合理的 TTL,把maxmemory设置得略低于物理内存,再配合淘汰策略兜底。
3.3 用监控指标判断“该不该清”和“清了有没有用”
判断 Redis 是否需要清理、清理后有没有效果,不能靠感觉,要靠指标。我会在清理前先记录三个关键指标:used_memory(当前内存使用量)、expired_keys(累计过期的 key 总数)、evicted_keys(累计被淘汰的 key 总数)。如果在INFO STATS里看到evicted_keys很短时间飙涨了几万甚至几十万,说明内存早就满了,淘汰策略已经在拼命工作,这时候单纯清缓存只是治标,你需要从源头降低内存使用速率。
清理结束后,重点观察keyspace_hits和keyspace_misses的变化。如果命中率下降得厉害,说明你很可能清掉了不应该清的数据,业务方会立刻感受到;如果命中率只是轻微下降然后快速回升,说明清理的大部分是无效缓存,清理方向是对的。此外,不要忽视INFO COMMANDSTATS里 del/unlink 命令的调用次数和耗时,如果 del 的每秒调用次数很高,你可能需要检查是不是有业务在反复写又反复删同样的 key,这种“抖动型写入”本身就会给 Redis 带来额外的负载。
4. 安全清理的完整实操流程:从预案到执行到复盘
4.1 制定清理预案,先想好“删错了怎么办”
清理缓存这件事,最大的风险不是技术方案不行,而是没有预案。我见过最典型的事故是:开发同学在测试环境写了个清理脚本,因为连错了 Redis 地址,直接在生产实例上执行了批量删除,导致线上会话大量失效,用户被迫重新登录,业务方炸了锅。所以无论你是清一百个 key 还是清一百万个 key,都要先建立一套“安全护栏”。
首要护栏是备份。Redis 层面没有像 MySQL 那样方便的 binlog 闪回,但你可以通过 RDB 文件做全量备份,或者提前把要删除的 key 列表和对应的 TTL 记录到本地文件。如果删除后出了问题,至少你能知道误删了哪些 key,可以手动重建或从下游数据源回源。我的习惯是:凡是批量删除规模超过一百个 key,先执行一次redis-cli --scan --pattern "目标前缀" > /tmp/keys_to_delete.txt,把 key 列表保存下来,再在脚本里逐行读取执行删除。这样即使出问题,你也能用备份文件快速恢复。
执行时机也很重要。虽然SCAN+UNLINK对主线程的影响较小,但大批量删除仍会占用 Redis 的 CPU 和内存分配器,所以尽量选业务低峰期执行。如果你无法确定高峰期,稳妥的做法是先清一小批观察延迟变化,比如每次删除 1000 个 key,等 5 秒看INFO STATS的instantaneous_ops_per_sec和latency有没有明显波动,再继续下一批。
4.2 一个可以“抄作业”的分阶段清理脚本
下面我提供一个生产环境验证过的清理思路,你可以根据自己的实际场景调整。这里以清理前缀为temp:cache:*的 key 为例,分三个阶段操作:
阶段一:盘点,统计目标 key 的数量和内存占比
redis-cli --scan --pattern "temp:cache:*" > /tmp/temp_keys.txt wc -l /tmp/temp_keys.txt阶段二:分批删除,每批 200 个 key,批间休息 200 毫秒
while read -r key; do redis-cli UNLINK "$key" count=$((count + 1)) if [ $((count % 200)) -eq 0 ]; then sleep 0.2 fi done < /tmp/temp_keys.txt阶段三:清理后检查内存与命中率
redis-cli INFO memory | grep used_memory redis-cli INFO stats | grep keyspace_hits redis-cli INFO stats | grep keyspace_misses有一点我要强调:上面的 shell 循环对每一行 key 都执行一次redis-cli,在 key 数量很大的时候效率很低,因为每一次调用都要重新建立 TCP 连接。更高效的做法是使用redis-cli --pipe批量发送命令,但--pipe模式下你无法根据反馈动态调整删除节奏。我个人的取舍是:几千个 key 用循环没问题;几十万甚至上百万 key 时,我更喜欢用 Lua 脚本一次性传入一批 key 删除,或者直接用redis-cli --scan --pattern配合xargs分批执行,然后靠 Redis 的慢日志和延迟监控来确认是否安全。技术方案没有绝对的好坏,关键是你要理解每一步的代价,并准备好应对突发情况。
4.3 清理过程中常见的坑和排查速查表
我在多个项目里整理过一张“Redis 清理缓存常见问题速查表”,这里直接分享出来,都是实际踩过的:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 执行 UNLINK 后内存没有立刻下降 | key 太大或太多,异步线程还没清理完;或者内存碎片率高 | 稍等几分钟再观察,用INFO memory对比used_memory和used_memory_rss |
| 删除了 key,但业务还在报“缓存存在” | 有多个副本或客户端本地缓存了旧数据 | 确认该 key 是否在集群其他分片,或检查客户端缓存策略 |
| 删除操作让 Redis 延迟突增 | 一次性删除的 key 太多,或误用了 DEL 删大 key | 改用 UNLINK,分批处理,降低每批删除的 key 数量 |
| 清理后缓存命中率骤降 | 误删了热点缓存,或删除范围过大 | 立即从备份文件恢复 key,或让业务直接回源重建缓存 |
| 清完一批 key,内存很快又满了 | 业务写入速率太高,或 TTL 设置不合理,或存在大 key 持续被写入 | 排查写入源头,调整 TTL,考虑对大 key 做拆分 |
这里再说一个容易忽略的细节:当 Redis 删除大量 key 后,内存分配器可能不会把内存立即归还给操作系统,所以你在used_memory_rss里看到的物理内存占用可能依然很高。这不是清理没生效,而是内存碎片和分配器机制导致的。遇到这种情况不要慌,观察used_memory是否已经降下来,降下来了就说明数据本身已经释放了。如果你非常在意物理内存回落,可以考虑在低峰期执行MEMORY PURGE,但这个命令会消耗较多 CPU,不能频繁执行。
4.4 集群和哨兵场景下的清理注意事项
如果你管理的是 Redis Cluster,清理缓存的姿势和单机又有区别。集群模式下,key 会按 hash slot 分散到多个节点上,你在某个分片上执行redis-cli --scan --pattern,只能扫到当前节点的 key,没法跨分片统一扫描。要实现全集群清理,有两个思路:一是逐个分片执行扫描和删除,遍历每个主节点地址,然后汇总结果;二是用redis-cli -c连接集群,配合--scan --pattern时它会自动跳转到对应分片吗?这里有个常见误解,redis-cli -c的集群模式主要用于普通读写命令的 key 重定向,--scan本身不是严格按 hash slot 打的,所以更好的方式还是分片操作,保证每个分片都被覆盖到。
哨兵模式下,你要特别注意“只清理主库还是同时清理从库”的问题。正常做法是只在主库执行删除,然后让同步机制把删除操作传到从库;不要同时在主从上都执行一遍,不然可能因为主从切换引发不必要的数据不一致。比如当前主库 A 被清完,此时发生自动切换,原从库 B 升为主库,但 B 上其实还残留着一部分旧 key,除非 A 的删除操作已经完整同步过来,否则你的清理就会留下死角。
最后再分享一个个人经验
清理 Redis 缓存这件事,做了几年下来我最大的体会是:真正健康的 Redis 缓存体系,不应该频繁依赖人工清理。如果你每个星期都要手动清一次缓存,那说明你的 TTL 设计、key 粒度、淘汰策略和数据生命周期管理出了问题。合理的设计应该是:绝大部分缓存都设置有层次的 TTL,冷数据自动过期,热数据留在内存里被反复命中,maxmemory和淘汰策略只作为意外高峰的缓冲,人工清理只是极少数的异常救援手段。
另外一个小技巧,清理前可以先在测试环境用同样的前缀和同样的 key 规模模拟一遍,把 SCAN 的 COUNT 参数调到一个相对安全的范围(我常用 500 到 1000),观测耗时和 OPS 波动。等摸清了参数范围再上生产,能少踩很多坑。Redis 的很多危险操作,其实只要你多做一步验证、多想一层后果,就能避免成为别人口中的“事故案例”。希望这篇文章能帮你把 Redis 缓存清理从“玄学”变成“工程”。