直接用DEL逐个删除多Key会导致非原子性、脏数据和竞态问题;应使用Lua脚本结合索引结构(如SET)动态获取并批量删除,确保原子性与一致性。

为什么直接用 DEL 删除多 Key 会出问题
当业务中一个逻辑实体(比如用户 ID user:123)关联多个缓存 Key(如 user:123:profile、user:123:settings、user:123:cache_timestamp),如果用客户端逐个发 DEL 命令,中间任意一步失败或网络中断,就会留下脏数据。更麻烦的是,若同时有其他客户端在写这些 Key,还可能出现“删了一半又被新写入”的竞态——这不是原子性,只是顺序执行。
Redis 的 Lua 脚本天然运行在单线程中,且整个脚本执行期间不会被其他命令打断,这才是真正意义上的原子性保障。
EVAL 中如何安全获取并删除一批 Key
关键不是硬编码 Key 名,而是让脚本从索引结构(比如一个 SET 或 LIST)里动态读出待删 Key 列表,再批量清理。常见模式是:先用 SPOP 或 LRANGE + LTRIM 取出 Key 列表,再用 redis.call("DEL", ...) 删除,最后清空索引本身。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 必须用
redis.call("DEL", unpack(keys_to_del)),不能用redis.pcall—— 后者失败不中断脚本,但你通常希望删不干净就整体回滚 - 索引结构本身也要在脚本末尾删掉(比如
redis.call("DEL", KEYS[1])),否则下次调用还会拿到旧 Key - 如果索引是
SET,注意SMEMBERS返回无序结果,但不影响功能;若需确定性顺序(比如调试),可用SSCAN配合游标分批处理大集合
eval "local keys = redis.call('SMEMBERS', KEYS[1]); redis.call('DEL', unpack(keys)); redis.call('DEL', KEYS[1]); return #keys" 1 user:123:index
用 EVALSHA 复用脚本避免重复传输开销
每次 EVAL 都要传完整 Lua 字符串,网络和解析成本高。上线后应改用 EVALSHA:先用 SCRIPT LOAD 加载一次脚本,拿到 SHA1 值,后续只传这个哈希值即可。
- 首次加载:
SCRIPT LOAD "local keys = redis.call('SMEMBERS', KEYS[1]); ..."→ 返回"9e4f8a2c7d..." - 后续调用:
EVALSHA 9e4f8a2c7d... 1 user:123:index - 如果 Redis 重启导致脚本丢失,
EVALSHA会返回(error) NOSCRIPT No matching script. Please use EVAL.,客户端需捕获该错误并自动 fallback 到EVAL
Key 索引本身怎么建才利于原子清理
索引结构选型直接影响脚本健壮性。别用 HASH 存 Key 列表(因为 HGETALL 不保证顺序,且字段值可能含二进制数据,unpack() 会出错);也别把 Key 拼成一个大字符串再 split()(Lua 没原生 split,自己实现易出 bug)。
- 首选
SET:语义清晰,SMEMBERS安全返回字符串数组,DEL后自动释放 - 次选
LIST:适合有插入顺序要求的场景,但要用LRANGE 0 -1+LTRIM 1 0清空,比DEL多一次操作 - 避免在索引里存过期时间等冗余字段——索引只做一件事:明确列出“哪些 Key 属于这个实体”,越简单越可靠
最常被忽略的一点:索引 Key 的生命周期必须严格短于其所指向的业务 Key。如果索引 Key 过期了但业务 Key 还在,下次重建索引时可能漏掉旧 Key;反过来,如果业务 Key 先过期而索引还在,脚本删的时候 DEL 会静默忽略不存在的 Key,看似成功实则没清干净。

















