释放锁时“判断+删除”必须原子执行,因get和del非原子操作会导致误删其他客户端锁;唯一可靠方案是使用lua脚本,通过redis.call('get')校验归属并redis.call('del')删除,且value须全局唯一、脚本无阻塞逻辑、集群下需hash tag。

释放锁时“判断+删除”必须原子执行
因为 GET 和 DEL 是两个独立网络请求,中间存在时间窗口:A 客户端 GET 到锁值后,锁恰好过期被 Redis 自动清理,B 客户端立即加锁成功;此时 A 仍按旧值执行 DEL,删掉的是 B 的锁。这不是理论风险,而是高并发下必然发生的误删。
Lua 脚本是唯一能保证原子性的方案
Redis 在执行 Lua 脚本时会阻塞其他命令,整个脚本内所有 redis.call() 操作在服务端串行完成,不受客户端网络延迟、GC 暂停或进程崩溃干扰。关键点在于:
-
redis.call('get', KEYS[1]) == ARGV[1]必须用GET,不能用EXISTS—— 后者不返回 value,无法校验归属 - 脚本里必须用
redis.call('del', ...),不是redis.pcall—— 前者失败直接报错,后者会吞异常,掩盖误删失败 - 返回值必须严格判断是否等于
1L(Java)或1(Python),不能只看是否抛异常
不用 Lua 的常见错误写法
以下代码看似合理,实则危险:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
if redis.get("lock:key") == "client-a-uuid":
redis.delete("lock:key")
问题不止在非原子性。更隐蔽的是:
- 两次网络往返,RTT 波动可能放大竞态窗口
- 客户端本地时钟偏差、线程调度延迟会让“判断”和“删除”之间的时间不可控
- 连接池复用时,
GET和DEL可能落在不同连接上,Redis 甚至收不到第二个命令
容易被忽略的边界细节
即使用了 Lua,仍有三个硬伤常被跳过:
- 锁 value 必须是全局唯一标识(如
uuid + pid),不能是固定字符串或时间戳 - 脚本不能包含 sleep、重试、循环等逻辑 —— Lua 运行期间会阻塞 Redis 其他请求,超时触发
SCRIPT KILL - 集群模式下,如果锁 key 被分配到不同 slot,
EVAL会报CROSSSLOT错误;必须确保 key 使用 hash tag,例如{order}:123
真正难的不是写对那一段 Lua,而是让加锁、续期、释放、异常兜底全部落在同一套可验证的原子语义里。










