必须用lua脚本原子执行“校验value+删除”,因get+del拆分会导致时间窗口内锁过期被他人抢占,从而误删;exists无法校验持有者,仅lua中redis.call("get",keys[1])==argv[1]可安全判断并删除。

必须用 GET 判断锁值再 DEL,不能拆成两步,也不能用 EXISTS 替代 —— 否则一定误删。
为什么 GET + DEL 拆开就出问题?
常见错误是客户端先 GET 锁的 value,比对成功后再发 DEL。这两步之间存在时间窗口:比如 JVM Full GC、网络延迟、线程调度暂停,都可能导致锁已过期被 Redis 自动清除,但客户端仍按旧值执行 DEL,结果删掉了别人刚加的新锁。
Redis 6.0 并未改变这一底层行为,EXPIRE 和 DEL 仍是独立命令,无法靠服务端配置修复。唯一解法是把判断和删除压进一个原子上下文 —— 即 Lua 脚本。
redis.call("GET", KEYS[1]) == ARGV[1] 是唯一安全的判断方式
脚本里不能写 redis.call("EXISTS", KEYS[1]),因为 EXISTS 只返回 0/1,无法校验持有者身份;也不能用 redis.pcall 包裹 DEL 来“防错”,那会吞掉本该暴露的逻辑错误(比如 key 不存在却还去删)。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
GET返回nil时,跟ARGV[1]比较结果为false,脚本自然返回0,安全 - 如果 key 存在但 value 不匹配,同样返回
0,不删 - 只有完全匹配才调用
redis.call("DEL", KEYS[1]),返回1
Jedis 中调用 eval 的三个实操细节
Java 客户端最容易翻车的地方不在脚本本身,而在调用层:
- 脚本字符串必须定义为
static final String或从 classpath 加载,禁止字符串拼接(容易引入空格、换行、编码问题) - 务必检查
eval返回值是否为Long.valueOf(1),而不是只捕获异常 —— 返回0表示“没删成”,不是失败,是预期中的安全拒绝 - 若用
JedisPool,确保eval在单个Jedis实例上调用完成(Jedis 默认满足,但自定义连接池或分片逻辑可能破坏)
锁重入场景下,这个脚本直接失效
上面的脚本只解决「单次加锁、单次释放」。如果你业务需要可重入(同一个 client 多次 lock()),value 就不能只是 UUID,得是 "uuid:2" 这种带计数的格式,脚本也得改成:先 GET、解析 count、减一、为 0 才 DEL。
这会让 Lua 变复杂,且 redis.call("INCRBY") / redis.call("DECRBY") 在 Redis 6.0 中仍不支持原子性读-改-写组合。更现实的做法是:由业务层控制重入次数,避免把状态逻辑塞进 Lua。










