不能直接用setnx+expire实现锁,因为二者非原子操作,setnx成功后若进程崩溃会导致锁永久滞留;redis 2.6.12后可用set的ex+nx组合加锁,但解锁需lua脚本原子校验token后删除,避免误删他人锁。

为什么不能直接用 SETNX + EXPIRE 实现锁?
因为这两步不是原子操作,SETNX 成功后若进程崩溃或网络中断,EXPIRE 没执行,锁就永久滞留。Redis 2.6.12 之后支持 SET 的 EX 和 NX 参数组合,但仅适用于加锁;解锁仍需判断锁归属(避免误删他人锁),而 DEL 无法安全校验 key 对应的 value —— 这正是 Lua 脚本能解决的核心问题。
EVAL 执行加锁脚本的关键参数和边界条件
加锁脚本必须校验 key 是否存在、是否匹配当前客户端唯一标识(如 UUID),并支持设置过期时间防止死锁。常见错误是把 client ID 当作固定字符串硬编码进脚本,导致多实例冲突;正确做法是通过 EVAL 的 KEYS 和 ARGV 动态传入。
-
KEYS[1]是锁 key(如"lock:order:123") -
ARGV[1]是客户端唯一 token(推荐用redis.call("RANDOMUUID")生成或应用层生成) -
ARGV[2]是锁过期时间(单位秒,建议 10–30 秒,太短易误释放,太长影响吞吐) - 脚本返回 1 表示加锁成功,0 表示失败(已存在且不匹配 token)
示例脚本:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
if redis.call("exists", KEYS[1]) == 0 then
redis.call("setex", KEYS[1], tonumber(ARGV[2]), ARGV[1])
return 1
else
local val = redis.call("get", KEYS[1])
if val == ARGV[1] then
redis.call("expire", KEYS[1], tonumber(ARGV[2]))
return 1
end
return 0
end
解锁脚本必须用 lua 保证原子性,且不能依赖 GET + DEL
先 GET 再 DEL 有竞态:A 获取到 token,B 此时释放锁,A 接着删掉 B 刚设的锁。唯一安全方式是用 Lua 检查并删除一步完成。
- 解锁脚本只接受一个参数:
ARGV[1]即 client token - 必须严格比对
GET结果与 token,相等才DEL,否则返回 0 - 不要用
redis.call("del", KEYS[1])无条件删除 —— 这是最高频误操作
示例解锁脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
实际使用中容易被忽略的三个细节
一是锁 key 的命名必须具备业务粒度隔离性,比如订单操作用 "lock:order:{order_id}",不能全局共用一个 key;二是超时时间要略大于最长可能执行时间(含 Redis 网络延迟),否则自动释放后并发逻辑可能重入;三是 Lua 脚本在 Redis Cluster 模式下要求所有 key 在同一 slot,必要时用 {...} 包裹 key 名强制哈希对齐,例如 "lock:order:{123}"。










