直接用DEL删大量小Key会卡住Redis,因其为阻塞式命令,需主线程逐个释放内存并更新字典,期间所有读写请求排队等待,延迟可达秒级;若key带过期时间还需清理过期队列,Redis 6+默认仍同步释放,须显式用UNLINK。

为什么直接用 DEL 删除大量小Key会卡住Redis
因为 DEL 是阻塞式命令,一次删 10 万个 key,Redis 主线程就得挨个遍历、释放内存、更新字典结构——期间所有读写请求都得排队等。尤其当这些 key 分散在不同 slot(集群模式)或属于大 hash/set 的成员时,延迟可能飙升到秒级。
更麻烦的是:如果 key 带过期时间,DEL 还要顺带清理过期队列;如果是 Redis 6+ 启用了 lazyfree-lazy-user-del,DEL 默认仍走同步释放路径,除非显式加 UNLINK。
SCAN + Lua 脚本批量删的正确姿势
核心思路是:用 SCAN 渐进式遍历,每次只取一批 key(比如 100 个),再通过 Lua 脚本在服务端原子执行删除,避免网络往返和客户端逻辑判断开销。
关键点不是“用不用 Lua”,而是 Lua 里必须用 redis.call("DEL", ...) 而非 redis.pcall(后者失败不中断,但你删 key 几乎不会出错,没必要容错);同时要控制单次 SCAN 的 COUNT 参数,建议设为 50–200,太大仍可能抖动,太小则往返次数多。
示例 Lua 脚本(保存为 del_batch.lua):
local keys = redis.call('SCAN', tonumber(ARGV[1]), 'MATCH', ARGV[2], 'COUNT', ARGV[3])
for i, key in ipairs(keys[2]) do
redis.call('DEL', key)
end
return keys
调用方式(redis-cli):
redis-cli --eval del_batch.lua , 0 "user:session:*" 100
注意:--eval 后第一个逗号前后分别是脚本路径和 KEYS/ARGV 分界;这里没传 KEY,所以逗号前为空;0 是游标,"user:session:*" 是匹配模式,100 是 COUNT。
SCAN 匹配时容易忽略的三个坑
-
SCAN不保证一次扫完,必须循环调用直到游标返回"0"—— 很多人只跑一次就以为删完了 - 匹配模式里的
*是 glob 风格,不是正则;?匹配单字符,[abc]匹配字符集;写成"user:session:\d+"是无效的 - 如果 key 名含空格、冒号或特殊符号,
SCAN本身能正常返回,但 Lua 中直接DEL没问题;不过用 redis-cli 执行时,shell 可能提前解析引号或通配符,务必用单引号包裹 pattern
比 SCAN+Lua 更稳的替代方案:UNLINK + 渐进式调度
Redis 4.0+ 提供 UNLINK,它把 key 从数据库中摘除后,交由后台线程异步释放内存,主线程几乎不阻塞。对大批量小 key,这比 Lua 脚本更轻量、更可控。
实操建议:
- 先用
SCAN获取 key 列表(不删),存到本地临时数组 - 分批次调用
UNLINK key1 key2 ... keyN,每批控制在 100–500 个(避免单命令参数超限) - 加短延时(如 10ms),防止瞬时压力过大;可用
sleep或 client 端节流逻辑 - 监控
lazyfree_pending_objects指标,确认后台线程没积压
真正难处理的从来不是“怎么删”,而是“删的时候别让线上服务感知到”——SCAN 游标位置、Lua 执行时长、UNLINK 后台负载,三者得交叉观察,不能只盯一个指标。











