☰
Redis缓存清理避坑指南:从KEYS到UNLINK的实践
2026/10/13 12:31:46 网站建设 项目流程

生产环境处理过最糟心的一件事,就是半夜被叫起来,说商品详情页大量超时。排查半天,原因是同事在清理 Redis 缓存时直接敲了 KEYS,然后循环 DEL,几下就把 Redis 主线程堵死了。Redis 清理缓存这事,看着简单,好像就是个 DEL、FLUSHALL,但实际上从命令选型到集群环境,里面的坑比想象中多得多。这篇就把这些年我在 Redis 缓存清理上踩过的坑、用过的方案、确认过的原理一次性讲清楚。

1. 先认清需求:你要清的到底是哪种"缓存垃圾"

1.1 三种"清理"不是一回事

很多刚上手 Redis 的同学,一说清理缓存就是 FLUSHALL,但 Redis 里的"清理"至少包含三层含义,搞混了很容易出事。

第一层是主动删除。业务上明确知道某些数据已经失效了,比如商品下架、配置变更,需要立刻把对应的 key 删掉,保证客户端下次查询拿到的是新数据。这类清理是开发者在代码里手动触发,也是"清理缓存"最常指的操作。

第二层是过期删除。Redis 对设置了 TTL 的 key 有惰性删除和定期删除两种机制。惰性删除是客户端访问到这个 key 时才检查是否过期,过期了当场删掉返回空;定期删除是 Redis 每隔 100ms 随机抽取一部分带过期时间的 key,批量检查并删除。这一层是 Redis 自己在后台做的,不需要也不应该人工干预。

第三层是内存淘汰。当 Redis 内存达到 maxmemory 上限时,会按照配置的淘汰策略(LRU、LFU、noeviction 等)自动踢掉部分 key 来腾出空间。这同样是不需要人来管的,它跟业务理解的"清理缓存"完全是两码事。

把这三层分清楚之后,你才能判断当前的问题是需要主动删数据,还是只需要调大 maxmemory、改淘汰策略,又或者是业务代码里 TTL 设置不合理。我见过不少把内存淘汰误当成"缓存要爆了,赶紧清空"的情况,结果 FLUSHALL 一下去,缓存全没了不说,下游数据库直接被击穿。

1.2 什么时候非主动清理不可

既然过期删除和内存淘汰都是自动的,为什么我们还要主动清理缓存?因为 Redis 并不知道业务层面"这个数据已经不该存在了"。

举几个典型场景。商品在后台被下架了,但它的缓存 key 可能还有半个小时才过期,在这半个小时里用户依然能通过缓存看到已下架的商品,这就要主动把对应 key 干掉。再有,运营后台改了某个全局配置,配置类缓存如果还留着旧值,客户端拿到的一直是旧配置,这种也要主动清。还有业务数据被外部系统直接改了数据库,没有走我们自己的更新接口,缓存里的旧值没法靠 TTL 及时纠正,同样必须主动清理。

所以说,主动清理缓存的核心目的是解决"数据一致性"问题。你只有理解了这一点,才知道该用什么粒度的清理方式:是按 key 删,按前缀批量删,还是按业务模块清一个库。如果只是为了内存不够用,那优先考虑的是优化 key 的 TTL 和内存淘汰策略,而不是动不动就清库。

1.3 先给 Redis 做一次"体检"再动手

在真正执行清理之前,我强烈建议先摸清楚 Redis 当前的状态。用 INFO 命令看几个关键指标就够了。

  • used_memory:当前占用内存有多少
  • used_memory_peak:历史占用峰值,可以判断内存是不是一直涨
  • expired_keys:被过期删除的 key 数量变化
  • evicted_keys:因为内存上限被淘汰的 key 数量
  • keyspace_hits和keyspace_misses:命中率情况

如果evicted_keys在快速增长,说明内存确实不够了,主动清理只是应急,治本要靠 TTL 和淘汰策略。如果expired_keys很小但内存依然高,那大概率是有大 key 或者无 TTL 的 key 堆积,这种就要重点排查。

另外,我还习惯用DEBUG OBJECT或者MEMORY USAGE命令看看单个 key 的占用,特别是要清理的 key。如果一个 key 占了几个 GB,删除它和删除一个几 KB 的 key,代价完全不一样,后面会详细讲。体检之后,你才知道是用 DEL 直接删还是要用 UNLINK 异步删,避免把主线程拖垮。

2. 清理缓存前的原理课:为什么同样删个 key,差距能这么大

2.1 数据类型和内部编码决定了删除代价

很多人觉得删 key 就是一瞬间的事,实际上删除代价取决于 key 内部存了多少东西、采用什么编码。Redis 的 String 类型如果值很小,DEL 就是常数时间,毫秒级完成。但如果是几 MB 的大字符串,删除时要释放连续的内存块,这个过程虽然也是单线程操作,但耗时明显上升。

更要命的是集合类型。一个 Hash 里塞了上百万个字段,一个 List 有几十万个元素,Set 或者 ZSet 很大,这类 key 在 Redis 6.0 之前的版本里,DEL 是一口气把所有元素的内存都释放掉的,期间主线程被占用,其他所有命令都要排队等待。我遇到过一个大 Hash,大概有几百万字段,删除耗时接近一秒,那一秒线上相当于 Redis 被按了暂停键。

所以在清理之前,第一个动作是判断目标 key 是不是大 key。用MEMORY USAGE key看估算内存,或者用DEBUG OBJECT key看 serializedlength,心里有数再决定删除方式。另外一个常见误区是,认为 Hash 的字段很多就必须用 HSCAN 慢慢删,其实只要确认它是大 key,直接用 UNLINK 就能异步释放,不用手动拆。

2.2 过期策略和内存淘汰:Redis 自己已经在"自动清理"

REDIS 的过期删除不是到点就删那么精确。惰性删除依赖访问,如果一个 key 过期了但一直没人访问,它就一直占用内存;定期删除则每 100ms 跑一次,从带过期时间的 key 集合里随机抽一批检查,也就是说整个过期 key 集合很大时,清理速度是跟不上的。

这就产生了一个经典现象:Redis 内存已经超了,但expired_keys也没怎么涨,因为很多 key 其实早就过期了但还没被删掉。这时候你主动去清理,其实也不是胡乱删数据,而是帮 Redis "打扫卫生"。

再说内存淘汰。maxmemory-policy 可以设置成 allkeys-lru、volatile-lru、allkeys-lfu、volatile-lfu、noeviction 等。只要内存没到上限,淘汰策略不会生效,所以你会看到内存接近上限但还没有淘汰发生。这种时候如果业务没有设置 TTL 的习惯,volatile-lru 根本没用,只有 allkeys-lru 才会真正开始踢 key。理解这个逻辑,比单纯想着清理缓存更能解决内存问题。

2.3 UNLINK 异步删除:Redis 4.0 给我们留的后门

Redis 4.0 引入的 UNLINK 命令,是清理大 key 的救命稻草。它和 DEL 的区别在于:UNLINK 只是把 key 从键空间中摘除,真正释放内存的操作放到后台线程慢慢做。

这里想提醒一点,UNLINK 不是完全异步,对于很小的 key,它可能当场就删掉了,效果和 DEL 一样;只有遇到大内存对象时,才会走后台释放。所以它不会像 DEL 那样长时间卡住主线程,清理大 key 时优先用 UNLINK。但需要注意,Redis 4.0 以下没有 UNLINK,遇到老版本只能硬着头皮用 DEL,或者用渐进式删除,比如 HSCAN + HDEL 分批清理 Hash 字段。

我自己在清理线上大 key 时的习惯是:先MEMORY USAGE估算,如果超过 100MB,直接 UNLINK;如果版本太老,就写一个分批删除的小脚本,一次删一批字段,中间 sleep 一下,把主线程的阻塞时间摊薄。

3. 动手清理:命令选型、批量操作与可视化工具避坑

3.1 单条删除:DEL 和 UNLINK 怎么选

先给结论:普通 key 直接用 DEL,担心是大 key 就用 UNLINK。两者最直观的区别就是同步与异步释放内存。

命令阻塞情况适用场景版本要求
DEL同步删除,大 key 会阻塞主线程小 key、确认安全的普通 key所有版本
UNLINK异步释放内存,小 key 也可能同步删大 key、未知大小的批量 key4.0 及以上

别小看这个选择,我见过生产事故就是 DEL 一个大 List,耗时 800 毫秒,期间缓存的查询全部排队,接口 P99 从 20ms 直接飙到 800ms。换成 UNLINK 之后,同样的 key 删除瞬间完成,后台慢慢释放,对线上几乎没影响。

3.2 清空整个库:FLUSHDB 与 FLUSHALL 的正确打开方式

FLUSHDB 清当前库,FLUSHALL 清所有库。这两个命令在生产环境的危险程度不亚于 rm -rf /,但某些场景下确实会用到,比如测试环境重置、数据迁移后清旧库。

Redis 6.2 之后 FLUSHDB 和 FLUSHALL 都支持 ASYNC 参数,可以生成后台任务异步清库,避免一次性释放海量内存卡住主线程。比如FLUSHALL ASYNC。

即便如此,我还是建议生产环境对这两个命令做安全管理。可以用 RENAME 的方式禁掉命令,比如在配置文件里:

rename-command FLUSHALL "" rename-command FLUSHDB ""

同时把权限控制好,运维人员能执行就够了,业务研发默认不可用。另外,清库之前一定确认你没连错环境,最好用CLIENT LIST看一下当前连接的客户端,再在客户端里看SELECT的 db 编号,别在预发环境操作却以为连的是测试库。

清库还有一个副作用经常被忽略:主从架构下 FLUSHALL 会同步给从节点,从节点的内存也会被瞬间清空,如果从节点正在承担读流量,一样会引发缓存雪崩。所以清库前要评估对整个集群的影响,最好选在业务低峰期。

3.3 按前缀批量清理:为什么必须用 SCAN 而不是 KEYS

这是整个缓存清理话题里最核心的实操点。按前缀清理,比如清掉所有user:order:开头的缓存,很多人的第一反应是用 KEYS 拿到所有 key 再批量 DEL。

KEYS 的最大问题是它会阻塞主线程,因为 KEYS 需要遍历整个键空间,一次性返回所有匹配的 key。Redis 是单线程处理命令,这个遍历期间,其他所有读写操作全部排队。线上 keyspace 有几千万个 key 时,一次 KEYS 就能让 Redis 卡好几秒。

正确的姿势是 SCAN。SCAN 基于游标迭代,每次返回一部分 key,不阻塞主流程。基本用法:

SCAN 0 MATCH user:order:* COUNT 1000

第一次调用游标从 0 开始,返回结果里的游标值作为下一次 SCAN 的入参,一直循环到游标归 0,表示遍历完成。这里要注意 COUNT 不是一次性返回的条数,而是每次遍历的桶数,实际返回的 key 数量可能多也可能少,千万别当成 LIMIT 用。

拿到 key 列表之后,逐个 DEL 或者 UNLINK。如果 key 数量很多,可以配合 Lua 脚本保证分批删除的原子性,减少网络往返:

local keys = redis.call('SCAN', ARGV[1], 'MATCH', ARGV[2], 'COUNT', ARGV[3]) for _, key in ipairs(keys[2]) do redis.call('UNLINK', key) end return keys[1]

再在客户端循环调用这个脚本,每次把返回的游标传进去。这样就避免了 KEYS 的阻塞,又比逐条 DEL 快很多。

很多人还会问,SCAN 出来的 key 会不会重复?确实会。因为 SCAN 的游标遍历过程中,哈希表的扩容和缩容可能导致同一个元素被遍历到多次。所以如果你统计删除数量,看到总数略大于实际 key 数量是正常的,UNLINK 重复删同一个 key 也不会有副作用,幂等。

3.4 可视化客户端能不能用来清缓存

Redis Desktop Manager(现在叫 RESP.app)和 Another Redis Desktop Manager 这类工具,日常排查键值、看 TTL、看内存确实方便。但我不建议用可视化工具做批量清理,尤其在生产环境。

原因有几个。第一,GUI 的"批量删除"底层可能还是 KEYS 然后循环 DEL,有的旧版本甚至直接在客户端全量加载 key,操作起来 Redis 一样卡。第二,GUI 操作容易误点,一次选中几千个 key 点删除,比命令行事故更难回滚。第三,当你通过 GUI 连跳板机、连生产 Redis 时,它默认可能会去扫描所有库甚至 cluster 的所有节点,行为不可控。

可视化客户端的合理用法是排查定位:看看某个 key 的 TTL 还剩多少、值有多大、属于哪个类型,从而决定用哪个命令清。真正执行清理,还是回到命令行工具或者脚本,可控性高得多。

4. 集群环境下的缓存清理,比单机复杂在哪

4.1 主从架构:删除命令会不会"留尾巴"

主从架构下,主节点执行的 DEL、UNLINK、FLUSHDB 等写命令会同步到从节点,从节点会执行相同的删除。这本来没问题,但有两个点需要留意。

一是复制延迟。如果主节点删除了某个大 key,同步到从节点会有短暂的时延,这个期间从节点还能读到旧数据。所以在读写分离的场景下,清理完主节点缓存后,客户端如果很快切到从节点读,有可能拿到脏数据。应对办法通常是把短时间内的读请求继续导到主节点,或者干脆等一个复制延迟周期再恢复读流量。

二是从节点如果配置了replica-read-only no,允许在从节点直接写数据,那从节点上可能残留一些主节点并没有的 key。这种情况下只清主节点不够,还要在从节点上单独清一遍。清完之后注意从节点不会把删除同步回主节点,别指望双向一致。

4.2 哨兵和 Cluster:全局扫不到,别想着一条命令搞定

Redis Cluster 的数据按照 slot 分布在多个主节点上,每个节点只保存一部分 key。这就意味着 KEYS、SCAN 都不能在单个节点上拿到全量数据。

正确做法是对每个主节点逐个执行 SCAN。用 redis-cli 可以这样处理:

redis-cli -h 192.168.1.1 -p 6379 --scan --pattern "user:order:*" | xargs redis-cli -h 192.168.1.1 -p 6379 UNLINK

这是单节点版本,Cluster 下要依次对每个节点执行。也可以用 redis-cli 自带的--cluster相关子命令配合脚本遍历。复杂一点的话,写个多线程脚本连接所有主节点,并发 SCAN 和 UNLINK,效率高很多。

Cluster 环境下清理缓存最容易犯的错是使用redis-cli -c连接后执行 KEYS。-c只是客户端自动跳转 slot,但 KEYS 这种不需要 key 的命令会直接在当前节点执行,永远不会返回其他节点的 key。所以要么逐个节点处理,要么在客户端层面用CLUSTER NODES拿到所有主节点地址,循环执行。

另外一个要注意的是,Cluster 模式下大 key 删除依然会阻塞该节点的主线程,UNLINK 的优势在这里更加明显。因为 cluster 的一个主节点挂了,它负责的 slot 对应的读写都会受影响,范围比单机故障大得多。

4.3 Docker 部署:容器里的 Redis 怎么清才干净

现在很多项目用 Docker 部署 Redis,清理缓存的命令基本没区别,但有几个容器场景特有的问题。

第一,你得先进容器再执行命令。用docker exec -it redis容器名 redis-cli就能进入客户端。如果是 Docker Compose 部署的,容器名一般是项目名-redis-1这种格式,先docker ps确认。

docker exec -it redis-standalone redis-cli -a 密码

第二,Redis 的数据可能有持久化文件,比如 appendonly.aof、dump.rdb。FLUSHALL 只是清内存,不会删 AOF 文件。AOF 文件里可能还记录着历史写命令,如果配置了appendfsync,重启时 Redis 会重放 AOF。重点来了:FLUSHALL 本身也会写进 AOF,所以重启后它依然会执行那次清空,一般不会出现"清了又变回来"的问题。但如果你手动去删了 RDB 或 AOF 文件,再重启 Redis,那就会从磁盘上没有旧数据的文件重新加载,这个行为要心里有数。

第三,清理完大 key 之后,容器所在宿主机的内存释放可能不会立竿见影,因为 Redis 向 OS 申请的内存不一定马上归还。你看到容器内存占用没降,以为是没删干净,其实可能是内存碎片或者 allocator 还没释放。用INFO memory看used_memory和mem_fragmentation_ratio,能更准确判断。

5. 清理踩坑实录:这些问题我基本都遇到一遍

5.1 执行 KEYS 后 Redis 卡死,业务超时大片

这是最常见的生产事故。现象很明确:有人为了清一批带前缀的缓存,在客户端敲了KEYS abc:*,然后 Redis 主线程被遍历操作拖住,所有读写请求排队,接口超时告警刷屏。

排查时一看慢日志,全是 KEYS 命令,耗时几秒钟。解决方式很简单:停掉 KEYS 相关操作,改用 SCAN 分批遍历。如果 Redis 已经卡死到命令都不响应,只能等待遍历结束,或者用CLIENT KILL踢掉发起遍历的连接。

踩过这个坑之后,我的习惯是在 Redis 配置里直接禁掉 KEYS 命令,用 rename-command 把它改名或者禁掉,只保留 SCAN 可用,从根上防止误操作。

5.2 删完了内存没降,还以为是删错库

场景:用 DEL 清掉了大量 key,结果INFO memory一看,used_memory 几乎没降,于是怀疑是不是删错库了。

实际上,Redis 释放的内存不会立刻归还给操作系统。它可能被内存分配器保留,继续给后续写入使用。特别是使用 jemalloc 分配器时,删掉大 key 释放的内存块可能不满足某个对齐条件,暂时无法回收到 OS。再加上碎片率,used_memory和 RSS 的差距会很大。

判断到底删没删干净,看 keyspace 统计更直接:

INFO keyspace

看每个库的 keys 数量变化。如果 key 数量确实少了,说明删除执行成功,内存没降是分配器的问题,不用过度紧张。

5.3 缓存清完了,前端还是读到旧数据

这个场景也经常遇到。Redis 里 key 已经删了,但页面刷新后数据还是旧的。原因往往不在 Redis,而在应用层自己的一级缓存。

很多框架默认支持本地缓存,比如 Caffeine、Guava Cache,还有像 Spring Cache 的多级缓存。客户端先查本地缓存,本地缓存没命中才查 Redis。你清了 Redis,本地缓存可能还有值,客户端根本就没走到 Redis 那一层。

解决思路是:清理 Redis 缓存的同时,想办法通知各应用节点清本地缓存。比如发布一个"清缓存"的消息到消息队列,或者调用一个管理接口触发各节点清本地缓存。如果你们的架构里还有 CDN,那还要等 CDN 缓存过期,或者主动刷新 CDN 节点,这些都是清理缓存时容易忘掉的一环。

5.4 和分布式锁、计数器抢时间:清理缓存导致竞态

某个更新操作先删除 Redis 里的缓存,再更新数据库,是经典的缓存更新策略。但这里有个竞态:如果线程 A 删了缓存,另一个线程 B 立即查库并回写旧缓存,此时线程 A 的数据库更新还没提交,那缓存里就是旧数据。

另外,Redis 的 INCR、DECR 这类计数器如果也被"清缓存"误删,那累计数据直接归零,业务会出现严重的数据不一致。我帮人排查过一个"库存数字莫名其妙归零"的问题,最后发现是运维按前缀清理缓存时,把存库存的计数器 key 也一起删了。

清理缓存前,一定要确认 key 的用途。计数类、分布式锁类、限流类的 key,不能跟着缓存一起清。分布式锁的 key 如果被误删,多个线程可能同时拿到锁,出现并发问题。

5.5 序列化导致 key 看起来像乱码,手工删不了

Java 项目里如果用了 JDK 默认的 JdkSerializationRedisSerializer,key 会带\xAC\xED\x00\x05这种二进制前缀,在 Redis Desktop Manager 里看起来就是乱码。这种 key 用可视化工具根本没法手工匹配删除。

所以从一开始就要设计好序列化方式。生产上通常用 GenericJackson2JsonRedisSerializer 或 StringRedisSerializer 序列化 key,保证 key 可读、可匹配。如果已经线上积攒了乱码 key,先写脚本往出导,看看到底哪些逻辑在写,统一改成规范序列化之后再清掉旧的。

这里多说一句:清理缓存不是"删库跑路"性质的临时操作,它应该是一套规范化体系的一部分。每个业务 key 的命名规范、TTL 策略、哪些 key 允许被清、哪些不能清,都应该有文档和脚本沉淀下来。我见过做得好的团队,会专门维护一个"缓存清理脚本库",把按前缀清理、大 key 清理、多级缓存通知清理这些常见操作封装成可复用的工具,而不是每次出问题都靠人肉敲命令。

5.6 常见问题速查表

现象可能原因排查思路解决方案
KEYS 导致 Redis 卡死KEYS 全量遍历阻塞主线程看慢日志确认 KEYS 耗时禁用 KEYS,改用 SCAN 分批删除
删除后内存没降内存分配器未释放/碎片率看 INFO memory 和 keyspace 数量确认 key 数量已减少,等待内存回收
清了 Redis 还读到旧值本地缓存/多级缓存没清检查应用日志是否命中本地缓存发布清缓存消息,清各节点本地缓存
前缀清理误删计数器未区分业务 key 类型检查删除脚本的匹配规则建立白名单,防止误删非缓存类 key
大 key 删除耗时太长DEL 同步释放大量内存MEMORY USAGE 确认 key 大小改用 UNLINK 异步删除
Cluster 下只扫到一个节点每节点存部分 slot 数据确认 cluster nodes 列表遍历所有主节点执行 SCAN
FLUSHALL 后从节点也清空写命令同步复制检查副本状态尽量别在生产用 FLUSHALL

6. 最后一点个人体会

清理缓存这件事,做一次容易,难的是每次都选对方案。我现在的习惯是先问三个问题:这个 key 能不能删、是不是大 key、删完会不会影响周边链路。然后才是决定用 DEL 还是 UNLINK、用 SCAN 还是直接清库。多花一分钟确认,好过半夜爬起来处理线上故障。希望这篇能把常见坑都给你标出来,下次清理缓存前先翻一遍再动手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询