欢迎光临
我们一直在努力

Redis 分布式锁怎么实现?SET NX、Lua、Redisson 与 WatchDog

缓存击穿时,一个解决方案:使用互斥锁,只允许一个请求重建缓存,其他请求等待。

但在实际项目中,往往不止一个服务实例。例如:

Nginx
↓
┌────────┼────────┐
↓ ↓ ↓
服务 A 服务 B 服务 C
│ │ │
└────────┼────────┘
↓
MySQL

如果三个服务同时操作同一个资源,普通的 Java synchronized 或 ReentrantLock 就不够用了。

因为它们只能协调同一个 JVM 内的线程,无法让多个独立进程共享一把锁。

因此我们需要一种跨进程的互斥机制,也就是分布式锁(Distributed Lock)。

Redis 就是实现分布式锁的常见方案之一。


1. 为什么需要分布式锁?

假设我们正在开发一个订单系统。

现在有一个定时任务,负责扫描超时未支付订单并关闭。

系统部署了三个实例:

订单关闭任务
↓
┌────┼────┐
↓ ↓ ↓
A B C

如果三个实例都执行相同任务,可能同时处理订单 1001。

例如:

服务 A:
查询订单 1001 → 未支付

服务 B:
查询订单 1001 → 未支付

服务 A:
关闭订单 1001

服务 B:
再次关闭订单 1001

如果关闭订单还伴随着释放库存、发送通知等操作,就可能造成重复处理。

我们希望实现:

同一时刻,对于订单 1001,只允许一个服务实例执行对应的业务逻辑。

这时就可以使用分布式锁。

例如把锁的名称设计为:

lock:order:1001

所有实例处理这笔订单前,都先尝试获取这把锁。

服务 A ──┐
服务 B ──┼──→ Redis 分布式锁
服务 C ──┘ ↓
只有一个成功

获取成功的实例执行任务,其他实例等待、重试或者直接放弃。

不过需要提前强调:分布式锁是并发协调手段,不应该替代数据库约束、事务和幂等性设计。 后面会详细解释。


2. 分布式锁应该具备哪些特性?

一个实用的分布式锁至少要考虑以下几个问题。

互斥性: 在锁的有效期内,同一资源原则上只能被一个客户端持有。

防止死锁: 如果持锁进程突然崩溃,锁不能永远不释放。

安全释放: 只能释放自己持有的锁,不能删除其他客户端的锁。

超时控制: 获取锁不能无限等待,业务也要考虑锁过期的情况。

故障处理: Redis 宕机、网络中断、服务长时间暂停时,需要明确系统能够提供什么程度的正确性保证。

这几个条件中,前面三个是 Redis 分布式锁最基础的设计目标。


3. SETNX 为什么可以实现互斥?

Redis 提供了一个命令:

SETNX,全称是 SET if Not eXists。

意思是:

只有 Key 不存在时,才允许设置成功。

例如客户端 A 执行:

SETNX lock:order:1001 A

如果这个 Key 原本不存在,Redis 返回 1,表示设置成功。

然后客户端 B 执行:

SETNX lock:order:1001 B

由于 Key 已经存在,Redis 返回 0,表示设置失败。

整个过程:

客户端 A Redis

SETNX lock A ──────────────→ Key 不存在
↓
创建成功
←────────── 返回 1

客户端 B Redis

SETNX lock B ──────────────→ Key 已存在
↓
创建失败
←────────── 返回 0

因为 Redis 会原子地执行这条命令,所以不会出现两个客户端都在同一个 Key 不存在时成功设置的情况。

于是就实现了基本的互斥。

但是这种写法存在一个严重问题。


4. SETNX 最大的问题:锁可能永远不释放

假设客户端 A 获取锁以后开始处理业务。

客户端 A
↓
SETNX 成功
↓
开始执行业务
↓
💥 服务突然崩溃

此时:

lock:order:1001

还存在于 Redis 中。

客户端 A 已经崩溃,根本没有机会释放锁。

于是客户端 B、C 再次请求:

SETNX
↓
始终失败

最终造成:

其他客户端永远无法获取这把锁。

这实际上就是死锁风险。

所以不能只使用 SETNX,还需要给锁设置过期时间。


5. 为什么 SETNX + EXPIRE 仍然不够?

最容易想到的解决办法是:

SETNX lock:order:1001 A
EXPIRE lock:order:1001 30

这样锁最多存在 30 秒。

但是这两条 Redis 命令之间存在一个问题:

它们不是一个原子操作。

假设执行顺序是:

客户端 A
↓
SETNX 成功
↓
准备执行 EXPIRE
↓
💥 进程突然崩溃

由于 EXPIRE 还没有执行,Redis 中的锁就没有设置过期时间。

最终仍然可能出现永久锁。

因此:

加锁和设置过期时间必须在同一个原子操作中完成。


6. 正确的基础加锁方式:SET NX PX

现代 Redis 更常使用一条 SET 命令完成加锁。

SET lock:order:1001 token-A NX PX 30000

解释一下:

参数含义
lock:order:1001 锁的 Key
token-A 本次加锁的唯一标识
NX Key 不存在才设置
PX 30000 过期时间为 30000 毫秒

这里可以把它理解为:

检查 Key 是否存在
+
设置锁的持有者标识
+
设置过期时间
↓
一个原子命令

如果返回 OK,说明获取成功;如果条件不满足,则表示没有获取到锁。

因此,即使客户端突然崩溃,只要 Redis 正常处理过期机制,锁也能在 TTL 到期后自动释放。

Redis 官方同样推荐以 SET key randomValue NX PX ttl 作为单实例分布式锁的基础加锁形式。

参考:Redis 官方分布式锁文档


7. 为什么锁的 Value 不能随便写?

前面的示例使用:

token-A

但实际开发中,通常应该使用随机生成的唯一标识,例如 UUID。

SET lock:order:1001 "a7d3e9f1-…" NX PX 30000

注意,这个唯一标识最好是每一次加锁操作独有的,而不只是每个服务实例一个固定 ID。

为什么?

因为 Redis 锁不仅需要知道:

现在有没有人持有锁?

还需要知道:

这把锁当前到底属于哪一次加锁操作?

这个标识在释放锁时非常重要。


8. 为什么不能直接 DEL 释放锁?

这是 Redis 分布式锁中最经典的问题。

假设客户端 A 成功加锁,TTL 为 10 秒。

00 秒:
客户端 A 获取锁成功

01 秒:
客户端 A 开始业务

10 秒:
客户端 A 还没执行完
锁自动过期

11 秒:
客户端 B 获取同一把锁

12 秒:
客户端 A 执行完毕
准备释放锁

如果客户端 A 直接执行:

DEL lock:order:1001

就会发生:

客户端 A 的锁已过期
↓
客户端 B 已获取新锁
↓
客户端 A 执行 DEL
↓
客户端 B 的锁被删除

这叫:

锁误删。

更严重的是,客户端 C 接下来也可能获取锁成功。

最终可能出现:

客户端 B 正在执行业务
+
客户端 C 也开始执行业务

所以释放锁时,不能只根据 Key 名称删除。

必须先检查:

Redis 中当前保存的锁标识,是否仍然属于自己。


9. 为什么 GET + DEL 仍然不安全?

有人会这样写:

GET lock:order:1001
↓
判断 Value 是否等于自己的 token
↓
如果相等,就 DEL

逻辑看起来正确,但仍然有问题。

因为:

GET 和 DEL 是两条命令,并不是一个原子操作。

例如:

客户端 A:

GET → 发现 token 还是自己
↓
暂停

Redis:

A 的锁过期
↓
客户端 B 获取新锁

客户端 A:

恢复执行
↓
DEL
↓
删除 B 的锁

因此即使先检查 Value,也无法保证在执行 DEL 时,这把锁仍然属于自己。

正确的方法是:

把检查锁归属和删除锁合并成一个原子操作。

这就是 Lua 脚本的作用。


10. Lua 脚本为什么能安全释放锁?

Redis 可以执行 Lua 脚本。

例如:

if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end

其中:

  • KEYS[1] 是锁的 Key。
  • ARGV[1] 是当前客户端持有的唯一 token。
  • redis.call("GET", …) 读取锁的 Value。
  • 如果 Value 匹配,才执行 DEL。

使用时可以通过 EVAL 传入 Key 和 token:

EVAL 'if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end' 1 lock:order:1001 token-A

Lua 的关键优势不是语法本身,而是 Redis 会把脚本中的操作作为一个不可被其他客户端命令插入的执行单元。

因此:

检查锁归属
+
删除锁
↓
原子执行

这样就不会发生“检查时还是自己的锁,删除时已经变成别人的锁”的问题。

当然,Redis 脚本执行期间会阻塞其他命令处理,所以不应该在 Lua 中执行耗时循环或复杂计算。


11. Redis 8.4 以后还有更直接的命令

这里补充一个版本相关的新知识点。

Redis 8.4 引入了 DELEX,支持带条件删除 Key。

例如:

DELEX lock:order:1001 IFEQ token-A

意思是:

只有当前 Value 等于 token-A 时,才删除这个 Key。

这就可以直接完成“比较 + 删除”,不必自己编写 Lua 脚本。

但是需要注意:

  • Redis 8.4 及以上才支持该命令。
  • 较老版本仍然可以使用前面的 Lua 脚本。
  • Redisson 等客户端框架已经封装了自己的锁实现,不需要为了释放锁再手写一套 DELEX 逻辑。

参考:Redis DELEX 官方文档


12. 加锁成功以后,业务执行超过 TTL 怎么办?

我们已经解决了两件事:

  • 使用 SET NX PX 原子加锁。
  • 使用 Lua 或 DELEX 安全释放自己的锁。
  • 但还有第三个问题。

    假设:

    锁 TTL = 10 秒

    业务实际执行时间 = 30 秒

    那么:

    00 秒:
    客户端 A 获取锁

    10 秒:
    锁自动过期

    11 秒:
    客户端 B 获取锁

    11~30 秒:
    客户端 A 仍然在执行业务

    此时 A 和 B 就可能同时修改同一资源。

    注意:

    唯一 token + Lua 安全释放,只能防止误删其他客户端的锁,并不能防止旧持锁客户端继续执行业务。

    因此必须认真考虑锁的有效期。


    13. 直接把 TTL 设置得非常长行不行?

    例如我们把锁设置为:

    SET lock:order:1001 token-A NX EX 3600

    锁最多存在一小时。

    这样虽然降低了业务执行过程中提前过期的概率,但又带来另一个问题。

    如果客户端刚拿到锁就崩溃:

    获取锁
    ↓
    服务崩溃
    ↓
    其他请求可能要等待很长时间

    所以 TTL 太短容易提前失效,太长又会影响故障后的恢复速度。

    我们真正希望的是:

    业务仍然正常运行时自动延长锁的 TTL;一旦持锁进程停止,续期也停止,让锁最终自动过期。

    这就是自动续期机制。


    14. 什么是 WatchDog?

    WatchDog 中文通常叫看门狗。

    在 Redis 分布式锁场景中,它主要负责:

    持锁客户端仍然正常运行、并且锁仍属于自己时,定期延长锁的过期时间。

    例如:

    客户端 A 获取锁
    ↓
    初始 TTL = 30 秒
    ↓
    业务仍在执行
    ↓
    WatchDog 续期成功
    ↓
    TTL 再次延长
    ↓
    业务继续执行
    ↓
    再次续期
    ↓
    业务完成
    ↓
    释放锁

    这样就不需要提前猜测业务到底执行 10 秒还是 30 秒。

    但是 WatchDog 不是 Redis SET NX PX 自带的功能,而是需要客户端实现。

    Java 项目中,最常见的实现之一就是 Redisson。


    15. 什么是 Redisson?

    Redisson 是一个基于 Redis 的 Java 客户端框架,它在普通 Redis 命令之外,提供了分布式锁、集合、队列、信号量等高级对象。

    例如:

    RLock lock = redisson.getLock("lock:order:1001");

    就可以得到一个分布式锁对象。

    使用方式与 Java 自带的锁非常相似:

    RLock lock = redisson.getLock("lock:order:1001");

    lock.lock();

    try {
    // 执行业务逻辑
    } finally {
    lock.unlock();
    }

    但两者存在一个本质区别。

    Java 的 ReentrantLock 主要协调同一个 JVM 内部的线程,而 Redisson 的 RLock 通过 Redis 协调不同服务实例中的线程。

    Redisson 官方文档说明,RLock 是可重入分布式锁,并通过 Redis 的 Pub/Sub 机制通知等待获取锁的客户端。

    参考:Redisson 官方锁文档


    16. Redisson 的 WatchDog 怎么工作?

    Redisson 默认提供锁续期机制。

    当使用没有显式指定固定租约时间的加锁方式,例如:

    lock.lock();

    Redisson 会启用 WatchDog。

    默认的 lockWatchdogTimeout 为 30 秒,可以通过配置修改。

    注意,这个 30 秒不是说业务只能运行 30 秒。

    而是:

    每次成功续期后,锁会继续获得相应的有效期。

    可以理解:

    获取锁
    ↓
    TTL = 30 秒
    ↓
    客户端仍然持有锁
    ↓
    WatchDog 定期续期
    ↓
    TTL 延长
    ↓
    继续执行业务

    当客户端正常调用 unlock() 后,锁被释放,也就不再需要续期。

    如果客户端进程崩溃,续期无法继续,锁最终会因为 TTL 到期而失效。

    实际续期必须成功发送到 Redis 才有效。客户端长时间暂停、网络中断或 Redis 故障,都可能导致续期失败。


    17. Redisson 指定 leaseTime 后还会自动续期吗?

    这是非常容易答错的地方。

    看两种写法。

    第一种:

    lock.lock();

    没有显式设置固定租约时间,默认会启用 WatchDog。

    第二种:

    lock.lock(10, TimeUnit.SECONDS);

    这里显式指定:

    leaseTime = 10 秒

    表示锁在指定时间后自动释放。

    这种显式指定固定 leaseTime 的写法,不再依赖默认 WatchDog 自动续期。

    所以如果业务执行 20 秒:

    获取锁
    ↓
    固定租约 10 秒
    ↓
    10 秒后锁过期
    ↓
    业务可能仍在执行

    因此不能认为:

    只要用了 Redisson,就一定不存在锁提前过期问题。

    加锁方式会影响锁的生命周期。

    另外,tryLock 的参数也需要区分:

    waitTime 表示最多等待多久尝试获得锁。

    leaseTime 表示成功获得锁以后锁保持多久。

    这是两个完全不同的时间。


    18. Redisson 为什么支持可重入?

    前面我们说 Redisson 提供的 RLock 是可重入锁。

    所谓可重入,就是:

    同一个持锁线程可以再次获得自己已经持有的锁,而不会把自己阻塞住。

    例如:

    public void methodA() {
    lock.lock();
    try {
    methodB();
    } finally {
    lock.unlock();
    }
    }

    public void methodB() {
    lock.lock();
    try {
    // 业务操作
    } finally {
    lock.unlock();
    }
    }

    如果锁不支持可重入,methodA() 已经持有锁,再执行 methodB() 时,可能一直等待自己释放锁。

    而可重入锁会识别:

    当前申请者就是已经持有锁的线程。

    因此允许再次进入。

    Redisson 在内部会维护锁的持有者信息和重入计数。常见实现使用 Redis Hash 记录与客户端、线程相关的持有者标识及重入次数。

    可以简单理解:

    lock:order:1001

    持有者 A:线程 8
    ↓
    重入次数 = 2

    同一个线程每成功重入一次,计数增加;每解锁一次,计数减少。

    直到计数归零,才真正释放锁。

    因此:

    可重入不是允许所有线程重复拿锁,而是允许同一个持锁线程重复进入。


    19. Redisson 的 Pub/Sub 有什么作用?

    如果多个客户端竞争同一把锁,最简单的等待方式是不断轮询。

    例如:

    客户端 B
    ↓
    尝试加锁
    ↓
    失败
    ↓
    等待 100ms
    ↓
    再次尝试

    但是如果有大量客户端同时竞争,就会产生大量无意义的 Redis 请求。

    Redisson 的常规 RLock 会利用 Pub/Sub 通知等待者:

    客户端 A
    ↓
    持有锁
    ↓
    执行业务
    ↓
    释放锁
    ↓
    通过 Pub/Sub 通知
    ↓
    等待的客户端被唤醒
    ↓
    再次竞争锁

    这里需要注意:

    Pub/Sub 并不是把锁直接交给某个等待者。

    它只是一个通知机制。

    收到通知的客户端仍然需要重新竞争锁,所以普通 RLock 不保证严格按请求顺序获取锁。

    如果业务需要明确的公平排队语义,Redisson 还提供 FairLock 等不同类型。


    20. 一个完整的 Redisson 使用示例

    假设我们需要避免同一订单被多个实例同时处理。

    public void processOrder(Long orderId) {
    String lockKey = "lock:order:" + orderId;

    RLock lock = redisson.getLock(lockKey);

    boolean locked = false;

    try {
    // 最多等待 2 秒获取锁
    // 不指定固定 leaseTime,使用 WatchDog
    locked = lock.tryLock(2, TimeUnit.SECONDS);

    if (!locked) {
    throw new IllegalStateException("订单正在处理中");
    }

    // 加锁成功后,还需要检查订单最新状态
    Order order = orderMapper.selectById(orderId);

    if (!"PENDING".equals(order.getStatus())) {
    return;
    }

    // 真正执行业务操作
    processOrderBusiness(order);

    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    throw new IllegalStateException("等待订单锁时被中断", e);

    } finally {
    if (locked && lock.isHeldByCurrentThread()) {
    lock.unlock();
    }
    }
    }

    这个例子有几个重点。

    首先,锁 Key 要包含业务资源标识,避免把所有订单都锁到同一把锁上。

    其次,加锁成功后仍然要重新检查订单状态,因为在等待锁期间,其他线程可能已经把订单处理完了。

    再次,释放锁放在 finally 中,避免业务抛出异常时忘记释放。

    最后,tryLock(2, TimeUnit.SECONDS) 限制的是获取锁的等待时间,不是锁持有的固定租约时间。

    不过,这段代码只演示分布式互斥的基本用法。正式项目中,订单状态变更仍然应有数据库事务、状态条件更新和幂等约束配合,不能只依赖 Redis 锁。


    21. WatchDog 能彻底保证互斥吗?

    不能。

    这是理解分布式锁最重要的部分之一。

    假设客户端 A 获取锁:

    客户端 A
    ↓
    获取锁成功
    ↓
    开始执行业务

    随后发生一次很长的 JVM Stop-The-World GC。

    例如:

    客户端 A 停顿 40 秒

    在这段时间里,业务线程可能无法继续执行,WatchDog 的续期任务也可能无法正常运行。

    于是:

    客户端 A 暂停
    ↓
    无法及时续期
    ↓
    锁过期
    ↓
    客户端 B 获取锁
    ↓
    客户端 B 开始执行业务

    之后客户端 A 恢复:

    客户端 A 恢复运行
    ↓
    继续执行原来的业务

    问题来了:

    客户端 A 虽然已经失去锁,但它原来的代码可能仍然继续执行。

    WatchDog 不能自动把已经在运行的所有业务逻辑安全地撤销掉。

    所以:

    锁续期只能降低锁提前失效的概率,不能从理论上消除旧持锁者继续操作的问题。


    22. 什么是 Fencing Token?

    要进一步解决旧持锁客户端的问题,可以引入 Fencing Token(栅栏令牌)。

    它的核心思想是:

    每次成功获得锁,都得到一个单调递增的令牌,由真正被保护的资源拒绝旧令牌对应的操作。

    例如:

    客户端 A
    ↓
    获取锁
    ↓
    Token = 100

    然后 A 因长时间暂停导致锁失效。

    客户端 B 获取新锁:

    客户端 B
    ↓
    获取锁
    ↓
    Token = 101

    B 向数据库或其他存储系统提交更新,并携带 101。

    被保护的资源记录最新令牌。

    随后客户端 A 恢复执行,提交:

    Token = 100

    存储系统检查:

    当前最新 Token = 101

    客户端提交 Token = 100
    ↓
    100 < 101
    ↓
    拒绝旧请求

    这样,即使客户端 A 仍然认为自己可以执行操作,也不能覆盖 B 的新结果。

    注意:

    Fencing Token 必须由被保护的存储系统真正校验才有效。

    如果只是客户端生成一个递增数字,却没有在数据库更新、文件写入或外部服务操作时校验它,就没有真正实现 fencing。

    另外,令牌生成机制自身也必须保证可靠的单调性,不能因为 Redis 故障恢复就倒退或重复使用旧令牌。

    Redisson 当前还提供了 RFencedLock 之类的能力,用于这一类需要防范旧持锁者的场景。


    23. Redis 主从切换会不会导致锁不安全?

    有可能。

    假设 Redis 使用主从复制:

    Redis Master
    ↓
    Redis Replica

    客户端 A 在 Master 上成功加锁:

    客户端 A
    ↓
    Master 写入锁
    ↓
    返回成功

    但这个写入还没有复制到 Replica,Master 就崩溃了。

    随后:

    Replica 提升为新 Master
    ↓
    新 Master 没有 A 的锁
    ↓
    客户端 B 获取锁成功

    这样就可能出现:

    客户端 A 认为自己持有锁
    +
    客户端 B 也获取锁成功

    根本原因是:

    普通 Redis 主从复制并不是同步提交协议。

    Redis 官方分布式锁文档专门讨论了这种由于异步复制和故障转移引发的互斥性风险。

    需要注意,现代 Redisson 提供了副本同步检查等机制,可以降低相应的故障转移风险;但不能因此认为任何 Redis 部署方式都自动获得严格的分布式一致性保证。

    对于极高正确性要求的业务,仍然需要进一步考虑 Fencing Token、数据库约束或更强的协调机制。


    24. 什么是 Redlock?

    为了改善简单单实例 Redis 锁在故障情况下的局限,Redis 官方还提出了 Redlock 算法。

    它的核心思路是:

    在多个相互独立的 Redis 节点上尝试加锁,必须获得多数节点的认可,而且整个获取过程必须在锁有效期内完成。

    例如部署五个独立 Redis 节点:

    Redis 1
    Redis 2
    Redis 3
    Redis 4
    Redis 5

    客户端申请同一把锁:

    Redis 1 → 成功
    Redis 2 → 成功
    Redis 3 → 成功
    Redis 4 → 失败
    Redis 5 → 失败

    如果在允许的时间内成功获取至少三个节点上的锁,就可以认为符合多数派条件。

    不过还需要检查加锁耗时,扣除已经消耗的有效期,并在失败时释放已经获得的部分锁。

    所以 Redlock 并不只是:

    往五个 Redis 中 SETNX,成功三个就行。

    它还有时间约束、失败清理以及时钟假设等条件。

    更重要的是,Redlock 的正确性保证在分布式系统领域存在争论,Redis 官方文档也专门引用了相关分析,并提醒对一致性敏感的场景使用 Fencing Token 等保护。

    另外需要区分:

    Redisson 是 Java 客户端框架,而 Redlock 是一种分布式锁算法,二者不是同一个概念。普通 Redisson RLock 也不等于自动使用了 Redlock。

    对于一般的缓存重建和重复任务控制,不应该因为听说过 Redlock,就默认必须部署五个 Redis 节点。


    25. 分布式锁和数据库事务能互相替代吗?

    不能。

    假设我们在做库存扣减:

    SELECT stock
    FROM product
    WHERE id = 1001;

    发现库存还有 1 件。

    随后执行:

    UPDATE product
    SET stock = stock – 1
    WHERE id = 1001;

    如果多个请求同时执行,就可能产生超卖或错误扣减。

    有人可能会选择:

    先获取 Redis 锁
    ↓
    查询库存
    ↓
    扣减库存
    ↓
    释放锁

    但是更基础、通常也更可靠的方案,是让数据库直接进行条件更新:

    UPDATE product
    SET stock = stock – 1
    WHERE id = 1001
    AND stock > 0;

    然后检查影响行数。

    如果影响 1 行,说明成功扣减。

    如果影响 0 行,说明库存不足或条件不满足。

    这个例子中,数据库能够利用自身的并发控制保证单条条件更新的原子性,不一定需要额外引入 Redis 分布式锁。

    类似地,防止重复订单或重复消费,还可以通过:

    唯一索引

    状态机

    条件 UPDATE

    幂等键

    等方式保证最终正确性。

    因此:

    Redis 分布式锁适合减少并发冲突、协调跨实例任务,但不应该成为数据库业务正确性的唯一保障。


    26. 什么场景适合使用 Redis 分布式锁?

    Redis 锁比较适合跨实例协调、短时间互斥的场景。

    例如缓存击穿时的缓存重建:

    多个请求
    ↓
    一个请求获得锁
    ↓
    查询 MySQL
    ↓
    重建 Redis
    ↓
    其他请求读取新缓存

    还可以用于防止同一个定时任务在多个服务实例中同时执行,以及同一个文档的解析任务重复启动等。

    但是对于资金扣减、库存最终正确性、订单状态转换这些强业务约束,Redis 锁最多是额外的一层保护。

    系统应该先考虑数据库事务、条件更新、唯一约束和幂等性,再根据性能和业务需要增加分布式锁。


    27. 使用 Redis 分布式锁还应该注意什么?

    除了前面提到的互斥和安全释放,生产环境还需要关注一些实际问题。

    第一,锁的粒度。 如果所有订单都使用 lock:order,不同订单之间也会互相阻塞。更合理的方式通常是 lock:order:{orderId},只锁定同一业务资源。

    第二,获取锁的超时。 不能让大量请求无限等待锁,否则可能耗尽业务线程和连接资源。

    第三,重试策略。 高并发客户端同时失败、同时立即重试,容易形成竞争风暴。可以使用退避和随机延迟。

    第四,锁持有时间。 临界区应该尽量短,避免在锁内执行不必要的远程请求和耗时计算。

    第五,Redis 故障处理。 如果 Redis 不可用,要明确业务应该失败、降级还是采用其他协调机制,不能在加锁失败时悄悄继续执行需要互斥保护的操作。

    第六,幂等性。 无论锁设计得多好,都应该考虑业务重复执行时是否能够保持正确结果。


    28. 最容易搞错的几个地方

    第一,SETNX 具有原子性,不代表 SETNX + EXPIRE 两条命令整体具有原子性。 应该使用 SET NX PX 一次完成加锁和设置过期时间。

    第二,锁设置了 TTL,并不代表业务执行期间一定始终持有锁。 如果业务执行时间超过 TTL,其他客户端就可能获得新锁。

    第三,使用唯一 token 和 Lua 安全释放锁,并不能阻止旧客户端在锁过期以后继续执行业务。

    第四,WatchDog 不是万能的。 续期可能因为长时间 GC、网络异常、客户端暂停等原因失败。

    第五,Redisson 显式指定固定 leaseTime 后,不能再假设它会持续通过默认 WatchDog 自动续期。

    第六,Pub/Sub 只是通知等待者重新竞争锁,并不意味着普通 Redisson 锁是严格公平锁。

    第七,Redis 主从复制存在故障切换风险。 普通主从复制、Redisson 和 Redlock 不是三个完全等价的正确性方案。

    第八,分布式锁不能代替数据库事务、幂等性和业务状态校验。 尤其是资金、库存等场景,需要真正的数据约束保证最终正确性。

    –

    赞(0)
    未经允许不得转载:171主机测评 » Redis 分布式锁怎么实现?SET NX、Lua、Redisson 与 WatchDog
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址