1. 完整回答
Redis 分布式锁主要用于解决分布式环境下多个服务实例并发访问同一资源的问题,保证同一时刻尽量只有一个客户端执行关键业务逻辑。
比如多个服务实例同时处理同一笔订单,就可以通过分布式锁避免重复处理。
它的基本实现是利用 Redis 的 SET key value NX PX 命令。NX 保证只有 Key 不存在时才能写入,PX 设置过期时间,防止客户端宕机后锁一直无法释放。value 通常使用唯一请求标识,用来区分锁的持有者。
获取锁成功后执行相关业务,执行结束后校验锁的持有者并释放锁。释放时需要保证校验和删除的原子性,通常通过 Lua 脚本实现,避免误删其他客户端的锁。
实际 Java 项目中一般会使用 Redisson,它提供了可重入锁、自动续期和锁释放通知等功能。例如看门狗机制可以在业务执行时间较长时持续延长锁的有效期,降低锁提前过期的风险。
不过 Redis 分布式锁也有局限性。比如业务执行超过锁有效期,或者 Redis 主从切换导致锁数据丢失,都可能出现多个客户端并发执行的情况。
所以 Redis 锁比较适合并发任务协调、重复任务控制和共享资源互斥。对于库存扣减、资金操作等一致性要求较高的场景,还需要结合数据库原子更新、幂等控制或 Fencing Token 等机制,保证最终业务正确性。
2. 核心原理与关键概念
2.1 核心原理
Redis 分布式锁的核心是利用 Key 的唯一占用、过期时间和持有者身份校验实现互斥访问。
第一步:获取锁
SET lock:order:1001 unique_token NX PX 30000
-
lock:order:1001:锁名称,对应具体业务资源。 -
unique_token:每次加锁生成的唯一标识。 -
NX:Key 不存在时才能设置成功。 -
PX 30000:锁在 30 秒后自动过期。
返回 OK 表示获取成功,否则表示当前锁已被占用。
第二步:执行业务
获取锁成功 → 进入临界区 → 执行业务逻辑。
获取失败的客户端可以直接返回,也可以在限定时间内重试。
第三步:安全释放锁
业务结束 → 校验锁的唯一标识 → 标识一致才删除锁。
校验和删除必须是原子操作:
-
Redis 8.4 及以上:可以使用
DELEX key IFEQ unique_token。 -
较早版本:使用 Lua 脚本完成
GET比较和DEL删除。
第四步:异常处理
客户端宕机 → 无法主动释放 → Redis 根据 TTL 自动清理锁。
如果需要支持长时间运行的业务,可以通过自动续期机制延长 TTL。
2.2 关键概念
| 概念 | 作用 |
|---|---|
| 互斥性 | 同一把有效锁只允许一个客户端成功获取 |
| 原子性 | 加锁与设置过期时间、校验与释放锁都需要原子执行 |
| TTL(生存时间) | 防止客户端异常退出后长期占用锁 |
| 唯一标识(Token) | 标识锁的持有者,防止误删其他客户端的锁 |
| 看门狗(Watchdog) | 在满足续期条件时延长锁的有效期 |
| 栅栏令牌(Fencing Token) | 通过单调递增的版本号识别并拒绝过期持有者的操作 |
需要特别注意:Redis 锁的互斥保证受到租期、故障切换和客户端执行状态等条件限制。
3. 高频面试追问
3.1 为什么使用 SET NX PX,不能直接用 SETNX 加 EXPIRE?
主要原因是要保证获取锁和设置过期时间的原子性。
如果先执行 SETNX,再执行 EXPIRE,两个命令之间可能发生程序崩溃或者网络异常。
这时锁已经获取成功,但过期时间没有设置,导致其他客户端可能一直无法获取锁。
SET NX PX 可以通过一条 Redis 命令同时完成加锁和设置过期时间,避免这个问题。
3.2 为什么要设置唯一标识?直接 DEL 删除锁有什么问题?
唯一标识主要用于避免误删其他客户端持有的锁。
例如客户端 A 获取一把有效期为 10 秒的锁,但业务执行了 15 秒。
第 10 秒时锁自动过期,客户端 B 获取了同一把锁。此时 A 执行完成,如果直接调用 DEL,就会删除 B 的锁。
因此,每次加锁都需要生成唯一标识,释放时先检查标识是否一致,再删除。
同时必须保证检查和删除的原子性,可以使用 Lua 脚本,Redis 8.4 及以上也可以使用 DELEX IFEQ。
3.3 如果业务执行时间超过锁的过期时间,会发生什么?
可能发生多个客户端同时执行业务的问题。
例如 A 获得一把 10 秒的锁,但业务需要 15 秒。
锁在第 10 秒过期,B 就可能获取成功。这样在后续 5 秒内,A 和 B 都可能执行同一段业务。
常见解决方式有两种:
-
合理设置固定租期:适用于执行时间可预测的短任务。
-
自动续期:适用于执行时间不确定的任务,例如 Redisson 的看门狗。
不过自动续期也无法覆盖所有异常。如果客户端长时间 GC 暂停或者网络中断,续期仍然可能失败。
因此,对一致性要求较高的业务,还需要通过版本校验、Fencing Token 或数据库约束防止过期操作产生错误结果。
3.4 Redisson 的看门狗机制是怎么实现的?
Redisson 的看门狗主要用于防止业务还没有执行完成,锁就提前过期。
在使用默认自动续期方式获取锁时,Redisson 会设置一个初始租期,默认是 30 秒。
随后客户端会周期性执行续期任务,检查当前线程是否仍持有锁。如果仍然持有,就通过 Redis 脚本延长过期时间。
整个流程是:
获取锁 → 启动续期任务 → 校验持有者 → 延长 TTL → 释放锁后停止续期。
如果客户端宕机或者无法继续续期,锁最终会过期,避免永久占用。
继续追问:显式指定 leaseTime 后,还会自动续期吗?
通常不会。Redisson 在显式指定租期后,会按照指定时间自动释放锁,不启用默认看门狗续期机制。
例如 lock(10, TimeUnit.SECONDS),锁会在约 10 秒后过期,即使业务还没有执行完成。
3.5 Redisson 是怎么实现可重入锁的?
Redisson 通过 Hash 结构、线程身份标识和重入计数器实现可重入。
它会使用客户端实例标识和线程 ID 来识别持有者。
第一次获取锁时,创建对应的 Hash 字段,并把重入次数设置为 1。
同一个线程再次获取锁时,发现持有者身份一致,就可以直接重入,同时把计数器加 1。
释放锁时,计数器减 1。只有计数器归零,才真正删除锁,并通知等待的客户端。
这些判断和更新通过 Lua 脚本原子执行。
因此,同一个线程可以重复获取同一把锁,同时不会提前释放仍然处于重入状态的锁。
3.6 多个客户端竞争同一把锁,获取失败怎么办?
通常会根据业务特点选择立即失败或者限时等待重试。
对于允许快速失败的场景,可以直接返回繁忙状态。
对于需要等待的场景,可以使用 Redisson 的 tryLock,设置最大等待时间。
Redisson 普通锁会结合 Redis 的 **发布订阅机制(Pub/Sub)**处理等待。锁释放后发送通知,等待的客户端被唤醒,再重新竞争锁。
这样可以减少频繁轮询 Redis 带来的资源消耗。
如果采用自行实现的重试机制,通常需要设置最大重试次数或超时时间,并增加随机退避,避免大量请求同时重试。
3.7 Redis 主从切换会导致分布式锁失效吗?
有可能,主要原因是 Redis 主从复制通常是异步的。
例如客户端 A 在主节点获取锁成功,但锁数据还没有复制到从节点,主节点就发生故障。
从节点提升为新主节点后,因为缺少这条锁记录,客户端 B 也可能获取成功。
这样 A 和 B 就可能同时认为自己持有锁。
可以通过副本同步确认降低风险。Redisson 也提供了锁写入后的副本同步检查机制,但 Redis 的故障切换仍然不能提供严格的强一致保证。
继续追问:使用 WAIT 命令能完全解决吗?
不能。WAIT 可以等待指定数量的副本确认写入,提高数据安全性,但不能保证故障切换一定选择包含该写入的副本。
因此,副本同步确认可以降低风险,无法完全消除锁丢失的可能性。
3.8 Redlock 算法是什么?能彻底解决 Redis 分布式锁的问题吗?
Redlock 是 Redis 提出的一种基于多个独立 Redis 主节点进行多数派加锁的算法,用于降低单节点故障带来的风险。
以 5 个相互独立的 Redis 节点为例:
-
客户端记录开始时间,使用相同 Key 和随机标识向 5 个节点请求加锁。
-
如果至少 3 个节点获取成功,并且总耗时小于锁的有效期,就认为加锁成功。
-
实际剩余有效期还需要扣除获取锁的耗时和时钟偏差余量。
-
如果获取失败,就尝试释放各节点上已经创建的锁。
Redlock 可以提高对部分节点故障的容忍能力,但不能彻底保证业务操作的强一致性。
它仍然依赖时间和租期假设,也无法单独防止锁过期后旧客户端继续操作。
另外,Redlock 的安全性长期存在争议。当前 Redisson 已将 RedLock 对象标记为弃用,常规场景更适合使用 RLock,需要栅栏令牌时可考虑 RFencedLock。
3.9 什么是 Fencing Token?它和锁的唯一标识有什么区别?
Fencing Token 是每次成功获取锁时生成的单调递增令牌,用来阻止已经失去锁的客户端继续修改受保护资源。
例如客户端 A 获得令牌 100,随后因为长时间暂停导致锁过期。
客户端 B 获取新锁,令牌变为 101,并完成资源更新。
之后 A 恢复执行,携带令牌 100 请求更新资源。资源服务发现这个令牌小于已经处理过的 101,就拒绝操作。
它和普通唯一标识的区别在于:
-
唯一标识:主要验证谁有权释放当前锁。
-
Fencing Token:主要验证请求是否来自已经过期的持有者。
需要注意,令牌必须可靠地单调递增,并且由被保护的资源执行校验。仅在 Redis 中生成递增数字,无法自动实现这种保护;生成器还需要考虑故障切换后的计数回退问题。
3.10 实际项目中如何使用 Redis 分布式锁?怎样保证业务正确性?
实际项目中通常会使用 Redisson RLock 配合业务层的幂等控制。
例如防止同一笔订单被多个服务实例重复处理。
首先按照订单 ID 生成锁 Key,让同一订单的请求竞争同一把锁,不同订单可以并行处理。
获取锁成功后,先检查订单当前状态,再执行符合状态条件的更新。处理完成后,在 finally 中由持有锁的线程释放锁。
对于数据库更新,还可以配合状态条件、版本号或唯一约束,避免因锁失效而产生重复处理。
整个流程可以概括为:
按业务 ID 加锁 → 获取成功 → 校验业务状态 → 执行原子更新 → 释放锁。
另外需要控制锁粒度、等待超时和业务执行时间,并监控加锁失败、锁超时及续期异常。
对于库存扣减、资金变更等核心数据操作,数据库原子条件更新和事务约束通常是更直接的正确性保障;分布式锁主要负责协调并发流程。
4. 易混淆概念对比
4.1 SET NX PX 与 SETNX + EXPIRE
| 对比项 | SET NX PX | SETNX + EXPIRE |
|---|---|---|
| 操作方式 | 单条命令 | 两条命令 |
| 原子性 | 加锁和过期时间一起原子设置 | 默认无法保证整体原子性 |
| 异常风险 | 避免加锁成功但未设置过期时间 | 两条命令之间异常可能留下无过期时间的锁 |
| 使用建议 | 推荐 | 不建议直接组合使用 |
4.2 唯一标识与 Fencing Token
| 对比项 | 唯一标识 | Fencing Token |
|---|---|---|
| 核心作用 | 标识锁的持有者 | 识别过期持有者 |
| 生成方式 | UUID 等唯一随机值 | 可靠的单调递增序号 |
| 主要解决 | 误删其他客户端的锁 | 旧客户端继续操作资源 |
| 校验位置 | 锁服务 | 被保护的资源端 |
4.3 固定租期与看门狗续期
| 对比项 | 固定租期 | 看门狗续期 |
|---|---|---|
| 过期方式 | 到达指定时间后过期 | 持有锁期间周期性延长 TTL |
| 适用场景 | 执行时间可预测 | 执行时间不确定 |
| 优点 | 资源占用时间有明确上限 | 降低业务未完成时锁过期的风险 |
| 局限 | 业务超时可能失去锁 | 依赖续期任务正常运行 |
4.4 Redisson RLock 与 Redlock
| 对比项 | Redisson RLock | Redlock |
|---|---|---|
| 定位 | Redis 分布式可重入锁 | 多独立节点多数派加锁算法 |
| 节点要求 | 可运行在常规 Redis 部署上 | 需要多个独立 Redis 主节点 |
| 主要机制 | 持有者校验、重入计数、自动续期 | 多数派成功与剩余租期校验 |
| 使用情况 | 常规业务广泛使用 | 安全性存在争议,Redisson 对应对象已弃用 |
4.5 Redis 分布式锁与 ZooKeeper 分布式锁
| 对比项 | Redis | ZooKeeper |
|---|---|---|
| 核心机制 | 条件写入、租期和持有者校验 | 一致性协调、临时顺序节点和监听 |
| 性能特点 | 通常延迟较低 | 协调开销相对较高 |
| 故障处理 | 依赖过期和客户端续期 | 依赖会话、临时节点和集群一致性机制 |
| 一致性特点 | 常规主从部署存在锁丢失风险 | 提供更强的协调一致性基础 |
| 适用场景 | 高性能业务互斥、任务协调 | 对协调一致性要求较高的场景 |
ZooKeeper 也需要处理会话失效后旧客户端继续执行的问题,因此关键资源操作仍然可以结合 Fencing Token 等保护机制。
5. 总结
Redis 分布式锁主要用于协调多个服务实例对共享资源的并发访问。核心实现是通过 SET NX PX 原子获取带过期时间的锁,使用唯一标识和原子条件删除保证安全释放。实际开发中通常使用 Redisson,借助可重入机制、看门狗自动续期和发布订阅通知简化锁管理。它的主要优势是性能较好、实现灵活、适合分布式业务协调,但锁过期、客户端暂停以及 Redis 主从切换都可能破坏互斥效果。因此,对于一致性要求较高的核心业务,还需要结合数据库原子操作、幂等控制或者 Fencing Token,确保即使锁发生异常,业务数据仍然保持正确。