setnx单独使用无法防止重复提交,因其仅保证设键原子性,缺乏自动过期与安全释放机制,易导致锁残留、误删或死锁;必须配合ex过期与lua校验删除才能构成完整幂等方案。

SETNX 能防重复提交,但单独用它会丢数据、卡死、漏释放——它只是原子操作的起点,不是完整方案。
为什么 SETNX 单独用不行
它只管“设键”,不管“设完之后谁来删”:客户端崩溃、网络中断、业务异常退出,SETNX 设的锁就永远留在 Redis 里,后续所有请求都被拦住。这不是防重,是直接锁死接口。
- 返回
1表示抢到锁,但业务执行中途抛异常,没走删除逻辑 → 锁残留 - 返回
0表示锁已被占,但你没法知道这个锁是 2 秒前设的还是 2 小时前设的 → 无法判断是否该忽略或强删 - 没有超时自动清理机制 → 依赖应用层手动
DEL,而手动操作天然不原子
SET 命令带 EX + NX 才是正确起点
ThinkPHP 或任何 PHP 环境下,应弃用裸 SETNX,改用 SET key value EX seconds NX。这一个命令同时完成「不存在才设」「自动过期」两件事,原子性有保障。
-
NX保证不会覆盖已有值(防覆盖误删) -
EX指定秒级过期,比如EX 5表示最多卡住 5 秒,超时自动释放 - ThinkPHP 的
Cache::store('redis')->set($key, $value, 5)底层若调用的是SET ... NX EX,才安全;否则要确认驱动是否支持原子写
锁释放必须用 Lua 脚本校验 value
不能简单 DEL key:A 客户端设了锁,B 客户端在 A 还没执行完时超时了,自己又设了新锁,这时如果 A 直接 DEL,就把 B 的锁给删了 —— 出现“误释放”。
- 释放锁前,必须先
GET判断当前 key 的 value 是否等于自己当初设的 value(比如 UUID 或随机字符串) - 这个“判断 + 删除”必须原子,所以得用 Lua 脚本:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock_key abc123 - ThinkPHP 若没封装该逻辑,需自行调用
$redis->eval(...),不能拆成两步
实际业务中别只靠锁,加一层幂等标识更稳
Redis 锁解决的是“同一请求并发进来”的竞争,但挡不住用户刷新页面、重发请求、F5 提交这些场景——它们不是并发,是串行重复。这时候光靠锁没用,因为第二次请求来时,第一次的锁早没了。
- 前端提交时带一个唯一
request_id(如 UUID),后端存进 Redis,过期时间设为业务处理最大耗时 + 缓冲(比如 30 秒) - 每次请求先查
EXISTS request_id,存在就直接返回成功(或失败),不再执行业务逻辑 - 这个
request_id最好由服务端生成并返回给前端,避免客户端伪造;若前端生成,需配合签名防篡改
真正难的不是写对一行 SET 命令,而是想清楚:锁该设多久?超时后怎么兜底?重复请求到底算“失败”还是“静默成功”?这些决策点,比语法细节更容易出线上事故。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











