SETNX无法实现可重入锁,因其仅判断key是否存在,不校验客户端身份与计数;可重入要求同一客户端能多次加锁并按次数释放,需在value中存储client ID及递增计数,并通过Lua脚本原子性校验与更新。

为什么直接用 SETNX 无法实现可重入锁
因为 SETNX 只判断 key 是否存在,不关心当前客户端是否已持有该锁。同一个客户端重复加锁时,会被拒绝,而可重入锁要求:同一客户端可多次获取锁,只要释放次数匹配即可。更关键的是,单纯靠 key 存在与否,无法区分不同客户端——你得在 value 里存标识(比如 client ID),还得支持递增计数。
EVAL 脚本中如何安全地判断“是自己加的锁”并递增计数
核心逻辑是:先用 GET 拿当前锁 value,比对是否等于本客户端唯一标识(如 "client:123");若匹配,则用 INCR 更新计数;若不匹配或 key 不存在,则用 SETNX + EXPIRE 尝试新建锁(注意原子性,推荐用 SET key value EX seconds NX)。但 Lua 中不能直接调用 EXPIRE 后再 SETNX,所以全部逻辑必须收束在一次 EVAL 中。
实操建议:
- 客户端传入三个参数:
KEYS[1](锁 key)、ARGV[1](client ID)、ARGV[2](过期秒数) - 脚本开头用
local current = redis.call("GET", KEYS[1])读值 - 用
if current == ARGV[1] then判断是否为本客户端,是则redis.call("INCR", KEYS[1])(此时 value 可设为"client_id:count"格式,或单独用一个 hash 存计数——但为原子性,建议 value 直接存 JSON 或拼接字符串,如"client:123|2";更稳妥做法是用 hash:key 为锁名,field 为 client ID,value 为计数,用HINCRBY) - 新建锁时,务必用
redis.call("SET", KEYS[1], ARGV[1], "EX", ARGV[2], "NX"),这是 Redis 2.6.12+ 支持的原子写法,替代老式SETNX+EXPIRE的竞态问题
释放锁时为什么要用 EVALSHA 配合 SHA1 校验,而不是简单 DEL
因为释放锁必须满足两个条件:key 存在、且 value 等于本客户端 ID;否则可能误删别人持有的锁。Lua 脚本能做原子判断+删除,但每次 EVAL 传输脚本有开销。生产环境应先 SCRIPT LOAD 脚本获取 SHA1,再用 EVALSHA 执行,失败时降级回 EVAL。
典型释放脚本逻辑:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
if redis.call("HEXISTS", KEYS[1], ARGV[1]) == 1 then
local count = tonumber(redis.call("HGET", KEYS[1], ARGV[1]))
if count > 1 then
redis.call("HINCRBY", KEYS[1], ARGV[1], -1)
return 0 -- 未完全释放
else
redis.call("HDEL", KEYS[1], ARGV[1])
if redis.call("HLEN", KEYS[1]) == 0 then
redis.call("DEL", KEYS[1])
end
return 1 -- 已释放
end
else
return -1 -- 非法释放
end注意:这里用 HASH 存 client→count 映射,比拼接字符串更健壮;KEYS[1] 是锁主 key,ARGV[1] 是 client ID。
超时自动续期(watchdog)为什么不能只靠 EXPIRE 延长,而要重入式刷新
因为锁可能被其他客户端抢先删除(比如业务执行慢,锁过期了,另一客户端拿到并设置了新锁),此时你再 EXPIRE 就会延长别人的锁。必须确认“当前锁仍由自己持有”,才能续期。所以续期脚本也得先 HGET 校验 client ID,再 EXPIREAT 或重新 SET 过期时间。
容易踩的坑:
- 没做 client ID 校验就调
EXPIRE,导致锁被恶意延长 - 续期间隔太短,频繁刷 Lua 脚本增加 Redis 压力;太长又易过期。建议设为锁 TTL 的 1/3~1/2
- 客户端崩溃时 watchdog 无法运行,依赖原始 TTL 保底,所以初始 TTL 不能设得太短(通常 30s 起)
可重入锁的复杂点不在加锁,而在释放和续期时对“所有权”的持续验证——value 不只是标记,更是状态机的一部分。稍有疏忽,就会从“可重入”退化成“不可控共享”。

















