Redis SETNX 分布式锁为什么会误删:令牌校验、过期与释放脚本
来源:17golang原创
时间:2026-08-24 15:28:47 450浏览 收藏
订单超时任务偶尔会同时跑两次,排查 Redis 记录时却发现锁明明存在过。最容易忽略的情况是:持锁进程暂停超过租约,新进程已经拿到同一个锁,旧进程恢复后执行一条无条件 DEL,把新进程的锁也删掉了。
日常开发里用SETNX做Redis分布式锁,很容易碰到自己的任务卡了几秒、最后顺手删掉了别人刚拿到的锁的情况,问题根源根本不是SETNX本身有bug,而是锁的所有权校验、原子操作边界、过期规则这几块没做对。
实践要点
- SETNX 只负责竞争,锁值必须放入每次任务独有的随机令牌。
- 释放前要比对令牌,比较和删除必须由 Redis 原子完成。
- 过期时间是兜底边界,不是续期和业务完成确认的替代品。
常见的误删场景里,绝大多数情况都是没做锁持有者身份校验,直接用DEL命令删锁,或者校验令牌和删锁的两步操作中间插入了别的调度逻辑,最后动了不属于当前进程的锁。
先把误删现场还原出来
假设锁键是 lock:order-timeout。进程 A 抢到锁后暂停,租约 10 秒到期;进程 B 随后抢到锁并写入自己的令牌。此时 A 恢复,若仍执行 DEL lock:order-timeout,B 的任务就失去了互斥保护。

这条时间线里,A 的删除动作在语法上完全成功,但在所有权上已经失效。日志只记录“删除返回 1”并不能证明删除的是自己的锁,必须把令牌和持有者一起记录下来。
加锁时同时写入随机令牌和租约
Redis 版本支持 SET 的 NX 与 EX 组合时,优先使用一条命令完成抢锁和设置过期时间:
SET lock:order-timeout 7f3c...a91 NX EX 10
返回 OK 才代表本次任务拿到锁;返回空值表示已有持有者。令牌可以使用 UUID 或安全随机串,不能用固定的主机名、进程名,也不要让所有任务共用一个值。
为什么不建议分成两条命令
先 SETNX、再 EXPIRE 存在中间崩溃窗口:锁已经写入,却没有租约,最后变成永久占用。即便业务还在使用旧写法,也要把设置过期时间的失败纳入告警和人工清理流程。
释放锁要把比较和删除绑在一起
下面的 Lua 脚本把“读取当前值、比较令牌、删除键”放进一次 Redis 脚本调用。只有值仍然等于自己的令牌时,删除才会发生:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
end
return 0
在客户端中执行脚本时,KEYS[1] 传入锁键,ARGV[1] 传入本次任务保存的令牌。返回 1 表示确实释放了自己的锁,返回 0 可能意味着锁已经过期、被续期逻辑改变,或当前持有者不是自己;这三种情况都不应再补一条无条件删除。

续期和业务耗时要分开验收
任务耗时可能超过初始 10 秒时,可以按固定周期续期,但续期同样必须先校验令牌。否则 A 的续期请求在租约到期后到达,也可能延长 B 的锁。
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('expire', KEYS[1], ARGV[2])
end
return 0
续期周期要明显短于租约,并给暂停、网络抖动和 Redis 响应延迟留出余量。更重要的是,业务完成后仍要走带令牌校验的释放脚本,不要把“任务执行成功”误当成“当前进程仍拥有锁”。
用可复查命令做一次小实验
可以在测试实例中手工验证所有权边界。先让 A 写入令牌:
SET lock:order-timeout token-a NX EX 5
GET lock:order-timeout
TTL lock:order-timeout
等待过期后让 B 写入 token-b,再用 A 的令牌调用释放脚本,结果应为 0,且 GET lock:order-timeout 仍然返回 token-b。最后用 B 的令牌调用同一脚本,结果才应为 1。
几个容易误判的结果
TTL返回 -2 只表示键已不存在,不能证明某个进程释放成功。- 脚本返回 0 不等于 Redis 故障,先查看当前令牌和任务日志。
- 集群模式下,脚本涉及的锁键应保持在同一个哈希槽;不要把无关键塞进同一脚本。
常见问题与上线前检查
放在当前任务的内存变量中,并把任务编号写入结构化日志。不要把令牌放在全局固定配置里,否则多个任务无法区分所有权。
令牌应该放在哪里?
不能把租约当成业务授权。进程发现续期失败或租约已经失效后,应停止会产生副作用的后续动作;无法中止时,业务写入还需要幂等键或版本校验兜底。
过期后还能继续做业务吗?
确认加锁使用 NX 与过期时间,令牌每次唯一;释放和续期都通过令牌校验脚本;日志能区分抢锁、续期、释放的返回值;最后用 A/B 两个令牌复查“旧持有者不能删除新持有者”。
Redis 分布式锁的安全边界不在“有没有 SETNX”,而在每个动作是否持续证明自己仍是持有者。把令牌校验放进 Redis 原子脚本,再用幂等和版本检查承接租约失效,才算完成一条可验收的并发控制链路。
上线前检查什么?
-
366 收藏
-
117 收藏
-
426 收藏
-
171 收藏
-
113 收藏
-
370 收藏
-
数据库 · Redis | 1小时前 | Redis · 缓存 · 运维排查 · 过期策略 · PubSub · redis TTL 过期事件 keyspace notification notify-keyspace-events477 收藏
-
189 收藏
-
374 收藏
-
115 收藏
-
382 收藏
-
464 收藏
-
数据库 · Redis | 8小时前 | Redis · 消息队列 · 重试机制 · Stream · 消费组 · ACK · redis streams 重试 XREADGROUP XACK PEL pending339 收藏
-
数据库 · Redis | 19小时前 | Redis · 缓存 · 排行榜 · Sorted Set · 数据筛选 · redis 排行榜 BYSCORE Sorted Set ZRANGESTORE 窗口查询420 收藏
-
381 收藏
-
247 收藏
-
330 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习