生产环境禁用 KEYS+DEL,因其会阻塞 Redis 主线程;应使用带游标和分批的 SCAN+DEL Lua 脚本或 Java 中通过 RedisConnection 执行 SCAN 迭代删除,避免连接泄漏。
直接用 KEYS 配合 DEL 批量删前缀 key,会在高并发或大数据量时阻塞 Redis 主线程,生产环境必须避免。真正安全的做法是用 Lua 脚本封装 SCAN + DEL,既保证原子性又不阻塞服务。
为什么不能直接用 KEYS + DEL
KEYS 是全库扫描命令,会一次性把所有匹配 key 加载进内存并锁住主线程,期间所有读写请求都会排队等待。当匹配 key 超过几万个,Redis 可能卡顿数秒甚至触发超时熔断。线上出现过因 redis-cli KEYS "user:*" | xargs redis-cli DEL 导致订单接口平均延迟飙升到 2s+ 的事故。
常见错误现象包括:
-
ERR Error running script (call to f_...): @user_script:1: @user_script: 1: user_script:1: Lua script attempted to access a non-existent key(脚本里误用KEYS在集群模式下报错) - 监控显示
redis_blocked_clients突增,redis_latency_ms持续高于 100ms - 应用层大量
RedisCommandTimeoutException
EVAL 执行单次 Lua 脚本的隐患
像 EVAL "return redis.call('del', unpack(redis.call('keys', ARGV[1])))" 0 "cache:*" 这种写法,表面看是一条命令,但内部仍调用了阻塞式 KEYS,在 Redis 单线程模型下等同于直接执行 KEYS。
它的问题在于:
- 不支持 Redis Cluster:
KEYS无法跨 slot 执行,集群环境下直接报错 - 没有分批控制:如果匹配出 50 万个 key,
unpack()会尝试一次性传入DEL,可能触发 Lua 栈溢出或超时 - 无游标中断机制:一旦执行中失败(如网络闪断),无法从中断点继续
推荐方案:带游标和分批的 SCAN + DEL Lua 脚本
核心是用 SCAN 替代 KEYS,每次只取一批 key(比如 1000 个),再分小批次调用 DEL,全程不阻塞主线程。以下为可直接运行的脚本:
local pattern = ARGV[1]
local count = tonumber(ARGV[2]) or 1000
local batch_size = tonumber(ARGV[3]) or 100
local cursor = "0"
local total = 0
<p>repeat
local result = redis.call(``"SCAN"``, cursor, "MATCH"``, pattern, "COUNT"``, count``)
cursor = result[1]
local keys = result[2]</p><p>``if #keys > 0 then
for i = 1, #keys, batch_size do
local batch = {}
for j = i, math.min(i + batch_size - 1, #keys) do
table.insert(batch, keys[j])
end
redis.call(``"DEL"``, unpack(batch))
total = total + #batch
end
end
until cursor == "0"``</p><p>``return total
使用方式:
-
redis-cli --eval del_by_scan.lua , "user:session:*" 1000 50(逗号前是脚本路径,后是参数) -
ARGV[1]是匹配模式,必须带通配符,如"user:session:*" -
ARGV[2]控制每次SCAN返回数量,建议 100–1000;ARGV[3]控制每批DEL数量,建议 ≤ 100,避免单次命令过大 - 该脚本在 Redis 4.0+ 单机/集群均可用,集群下自动路由到对应 slot
Java 中用 RedisTemplate 调用 SCAN + DEL 脚本
Spring Boot 项目里别用 redisTemplate.keys(),改用原生连接执行 SCAN 迭代:
关键点:
omega1.swatchsh.com
rolex1.swatchsh.com
patek1.swatchsh.com
omegawx.paydyj.com
rolexwx.paydyj.com
patekwx.paydyj.com
omegawx.watchku.com
rolexwx.watchku.com
patekwx.watchku.com
omegawx.sitezj.cn
rolexwx.sitezj.cn
patekwx.sitezj.cn
omegawx.sepis.com.cn
rolexwx.sepis.com.cn
patekwx.sepis.com.cn
- 必须用
RedisConnection而非RedisTemplate,因为后者不暴露游标控制能力 -
ScanOptions.count()建议设为 100~500,太大会增加单次网络往返压力 - 每次迭代拿到 key 后,立即调用
connection.del(),不要攒成大集合再删 - 注意处理
Cursor关闭:用 try-with-resources 或显式cursor.close()
示例片段:
redisTemplate.execute((RedisCallback<Long>) connection -> {
long deleted = 0;
Cursor<byte[]> cursor = connection.scan(
ScanOptions.scanOptions()
.match(``"order:*"``)
.``count``(200)
.build()
);
try {
while (cursor.hasNext()) {
byte[] key = cursor.next();
connection.del(key);
deleted++;
}
} finally {
cursor.close();
}
return deleted;
});
真正容易被忽略的是游标生命周期管理——漏掉 cursor.close() 会导致连接泄漏,尤其在高频调用场景下,Redis 连接池会快速耗尽。