Redis 分布式锁的最小可用实现是两条命令:用一条 SET key value NX PX 毫秒 原子地抢锁,用一段 Lua 脚本“先比较 value、再删除”来放锁。真正决定它能不能用的,不是“这次抢到了”,而是三件事:抢不到锁的请求会不会退让、锁过期时业务还没跑完怎么办、释放时会不会把别人的锁删掉。下面按排查顺序给出可直接执行的命令、判断清单和验证方法。
Redis 分布式锁的可靠边界由三点决定:加锁必须是带 NX 和过期时间的单条 SET;value 必须是每次请求唯一的随机串,释放时用 Lua 比较后再删;锁的过期时间要覆盖业务最长执行时间,或配可续期机制。缺任何一条,都会出现两个请求同时进入临界区,或者误删他人的锁。建议先用 redis-cli 在目标实例上手工走一遍,再写进代码。
分布式锁:先把能核对的事实摆清楚
先确认对象:你要锁的是哪个资源粒度。锁键名建议带业务前缀和业务主键,例如 lock:order:1001,不要用固定常量,否则所有订单会抢同一把锁。锁值不要写死成 1,而要写每次请求生成的唯一串(UUID 或“机器名+线程号+随机数”),后面释放时靠它判断锁是不是自己的。
再确认前提条件。老一些的 Redis 版本可能不支持 SET 同时带 NX 和 PX,先在目标实例上执行一条试命令,看返回 OK/nil 还是语法错误,再决定是否改用 SETNX+EXPIRE 的两步写法(两步不是原子的,能升级就升级)。同时确认客户端连接的不是主从延迟很大的只读副本,否则抢锁结果本身就不可信。
可核对的现象集中在几处:GET lock:order:1001 返回当前持有者的 token;PTTL lock:order:1001 返回剩余毫秒数,-1 表示没有过期时间、-2 表示键不存在;EXISTS lock:order:1001 返回 0 或 1。客户端侧则看业务日志里打印的 token 和进入、退出临界区的时间戳。这两边对得上,讨论才有意义。
分布式锁最容易搞错的一点
最常见的误解是:SET NX 返回 OK 就说明“锁住了、安全了”。实际分界线在过期时间与业务耗时的关系上——锁 TTL 到了而业务还在跑,第二个请求就会拿到同一把锁,临界区里同时出现两个执行者;另一种情况是业务结束时用 DEL 直接删键,删掉的可能是别人刚抢到的锁。

一个能区分这两种情况的检查动作:业务执行到一半时,另开一个终端执行 redis-cli PTTL lock:order:1001,同时看业务日志是否还在输出。如果 PTTL 已经返回 -2(或很小)而日志还在跑,问题属于“锁提前过期”;如果 PTTL 正常、但在旧请求结束点观察释放脚本返回 0 而不是 1,问题属于“释放时没校验、删到了别人的锁”。这两个现象的处置方向完全不同,先分清再改代码。
分布式锁:一个照着就能做完的做法
以下以 lock:order:1001、超时 30 秒为例,在能连到目标 Redis 的机器上执行,TOKEN 换成你自己的随机串。
- 生成 token:
TOKEN=$(uuidgen),记下来,后面释放和日志都用它。 - 抢锁:
看到redis-cli -h 127.0.0.1 -p 6379 SET lock:order:1001 "$TOKEN" NX PX 30000OK表示抢到,看到nil表示已被别人持有,此时应当返回失败或稍后重试,不要继续往下执行业务。 - 抢到后立即复核一次:
redis-cli GET lock:order:1001应返回第 1 步的 token,PTTL lock:order:1001应是一个 0 到 30000 之间的正数。 - 执行临界区业务。若业务可能超过 TTL,需要另设续期逻辑(用同样“比较 token 再 PEXPIRE”的 Lua 脚本延长),续期失败就主动中止业务,不要假定锁还在。
- 释放。把下面脚本存成
unlock.lua:
执行:local v = redis.call('GET', KEYS[1]) if v == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0
返回redis-cli -h 127.0.0.1 -p 6379 --eval unlock.lua lock:order:1001 , "$TOKEN"1表示确实删掉了自己的锁,返回0表示 token 不匹配或锁已过期,此时不要重试删除,按“锁已失效”记录日志。
注意 KEYS 和 ARGV 之间是一个逗号加空格,参数顺序写错会得到意料之外的返回值。业务代码里对应用 try/finally 或等价结构,把释放放在 finally 中,避免异常路径漏放锁。
分布式锁:怎么验证结果对不对
验证分两层,先用命令验语义,再用并发验行为。
- 互斥性:开两个终端几乎同时执行带
NX PX的 SET,正常结果是一个返回OK、一个返回nil,不存在两个都是OK。 - 归属校验:用错误 token 执行释放脚本,正常返回
0,随后EXISTS lock:order:1001仍为1;换成正确 token 执行,返回1,EXISTS变为0。 - 过期生效:抢到锁后什么都不做,等待超过 30000 毫秒再查
EXISTS lock:order:1001,正常返回0,说明 TTL 真的生效,而不是写成了无过期时间的键。 - 业务侧:把进入和退出临界区的时间戳打进日志,并发跑一次后按资源主键排序检查,正常的日志是严格串行的,不会出现两条进入时间交叉的记录。
如果互斥性通过但业务日志仍出现重叠,重点查 TTL 是否小于业务耗时;如果 EXISTS 一直是 0,重点查是不是没有带 PX 参数,或者键名在两次调用之间不一致。验证到此为止,再考虑是否需要多实例或 Redlock 这类更复杂的方案,取决于你能接受的失败模式,而不是锁定实现本身的复杂度。