Redis分布式锁怎么实现

文章导读
Redis 分布式锁的最小可用实现是两条命令:用一条 SET key value NX PX 毫秒 原子地抢锁,用一段 Lua 脚本“先比较 value、再删除”来放锁。真正决定它能不能用的,不是“这次抢到了”,而是三件事:抢不到锁的请求会不会退让、锁过期时业务还没跑完怎么办、释放时会不会把别人的锁删掉。下面按排查顺序给出可直接执行的命令、判断清单和验证方法。
📋 目录
  1. Ⅰ 分布式锁:先把能核对的事实摆清楚
  2. Ⅱ 分布式锁最容易搞错的一点
  3. Ⅲ 分布式锁:一个照着就能做完的做法
  4. Ⅳ 分布式锁:怎么验证结果对不对
A A

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分布式锁怎么实现

一个能区分这两种情况的检查动作:业务执行到一半时,另开一个终端执行 redis-cli PTTL lock:order:1001,同时看业务日志是否还在输出。如果 PTTL 已经返回 -2(或很小)而日志还在跑,问题属于“锁提前过期”;如果 PTTL 正常、但在旧请求结束点观察释放脚本返回 0 而不是 1,问题属于“释放时没校验、删到了别人的锁”。这两个现象的处置方向完全不同,先分清再改代码。

分布式锁:一个照着就能做完的做法

以下以 lock:order:1001、超时 30 秒为例,在能连到目标 Redis 的机器上执行,TOKEN 换成你自己的随机串。

  1. 生成 token:TOKEN=$(uuidgen),记下来,后面释放和日志都用它。
  2. 抢锁:
    redis-cli -h 127.0.0.1 -p 6379 SET lock:order:1001 "$TOKEN" NX PX 30000
    看到 OK 表示抢到,看到 nil 表示已被别人持有,此时应当返回失败或稍后重试,不要继续往下执行业务。
  3. 抢到后立即复核一次:redis-cli GET lock:order:1001 应返回第 1 步的 token,PTTL lock:order:1001 应是一个 0 到 30000 之间的正数。
  4. 执行临界区业务。若业务可能超过 TTL,需要另设续期逻辑(用同样“比较 token 再 PEXPIRE”的 Lua 脚本延长),续期失败就主动中止业务,不要假定锁还在。
  5. 释放。把下面脚本存成 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 这类更复杂的方案,取决于你能接受的失败模式,而不是锁定实现本身的复杂度。