Redis如何批量删除特定前缀的Key_使用Lua脚本避免阻塞主线程

生产环境禁用 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 连接池会快速耗尽。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容