一把 Redis 分布式锁,踩透四个坑:锁争抢、僵尸锁、锁过期、锁丢失

Java 的 synchronizedReentrantLock 只能锁住单个 JVM,一旦服务多节点部署,JVM 之间互不可见,锁就完全失效了。本文从"为什么需要分布式锁"切入,按"问题 → 解决 → 引出新问题 → 再解决"的思路,系统讲清楚 Redis 分布式锁要解决的四大核心问题:锁争抢、僵尸锁、锁过期、锁高可用 ,以及对应的 SET NX EX、TTL 兜底、看门狗、UUID+Lua、RedLock 方案,最后讨论锁粒度和生产实践。


一、为什么需要分布式锁:Java 单机锁的失效

1.1 Java 内置的两种锁

在讲 Redis 分布式锁之前,先回顾 Java 自己的锁。Java 在 JVM 层面提供了两种主流的锁实现:

  • synchronized 关键字:JVM 内置的隐式锁,基于 Monitor 对象实现(对象头中的 Mark Word 标记锁状态),锁的获取和释放由 JVM 自动完成,不需要手动释放。JDK 6 之后引入了偏向锁、轻量级锁、重量级锁的升级机制,性能已经大幅提升。
  • ReentrantLock :JUC 包下的显式锁(java.util.concurrent.locks.Lock 接口的实现),基于 AQS(AbstractQueuedSynchronizer)框架实现,需要手动调用 lock()unlock(),支持公平锁、可中断、可超时、多条件变量等高级特性。

这两种锁有一个共同的前提:它们都只在单个 JVM 内生效。锁的状态存在 JVM 内存中(对象头或 AQS 的 state 变量),不同 JVM 之间互不可见。

1.2 单机锁在多节点部署下完全失效

对于客流量较大的服务,单机部署通常撑不住,业内通用的做法是多节点部署 + 负载均衡,把流量分摊到多个服务实例上。问题就出在这里。

以库存扣减为例,假设部署了服务 A 和服务 B 两个节点,前面挂一层负载均衡:
共享库存 服务B (JVM-B) 服务A (JVM-A) 负载均衡 用户B 用户A 共享库存 服务B (JVM-B) 服务A (JVM-A) 负载均衡 用户B 用户A #mermaid-svg-1W3M1hSfdyAvnDZu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1W3M1hSfdyAvnDZu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1W3M1hSfdyAvnDZu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1W3M1hSfdyAvnDZu .error-icon{fill:#552222;}#mermaid-svg-1W3M1hSfdyAvnDZu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1W3M1hSfdyAvnDZu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1W3M1hSfdyAvnDZu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1W3M1hSfdyAvnDZu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1W3M1hSfdyAvnDZu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1W3M1hSfdyAvnDZu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1W3M1hSfdyAvnDZu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1W3M1hSfdyAvnDZu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1W3M1hSfdyAvnDZu .marker.cross{stroke:#333333;}#mermaid-svg-1W3M1hSfdyAvnDZu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1W3M1hSfdyAvnDZu p{margin:0;}#mermaid-svg-1W3M1hSfdyAvnDZu .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1W3M1hSfdyAvnDZu text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-1W3M1hSfdyAvnDZu .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1W3M1hSfdyAvnDZu .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-1W3M1hSfdyAvnDZu .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-1W3M1hSfdyAvnDZu .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-1W3M1hSfdyAvnDZu #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-1W3M1hSfdyAvnDZu .sequenceNumber{fill:white;}#mermaid-svg-1W3M1hSfdyAvnDZu #sequencenumber{fill:#333;}#mermaid-svg-1W3M1hSfdyAvnDZu #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-1W3M1hSfdyAvnDZu .messageText{fill:#333;stroke:none;}#mermaid-svg-1W3M1hSfdyAvnDZu .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1W3M1hSfdyAvnDZu .labelText,#mermaid-svg-1W3M1hSfdyAvnDZu .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-1W3M1hSfdyAvnDZu .loopText,#mermaid-svg-1W3M1hSfdyAvnDZu .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-1W3M1hSfdyAvnDZu .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1W3M1hSfdyAvnDZu .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-1W3M1hSfdyAvnDZu .noteText,#mermaid-svg-1W3M1hSfdyAvnDZu .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-1W3M1hSfdyAvnDZu .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1W3M1hSfdyAvnDZu .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1W3M1hSfdyAvnDZu .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1W3M1hSfdyAvnDZu .actorPopupMenu{position:absolute;}#mermaid-svg-1W3M1hSfdyAvnDZu .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-1W3M1hSfdyAvnDZu .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1W3M1hSfdyAvnDZu .actor-man circle,#mermaid-svg-1W3M1hSfdyAvnDZu line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-1W3M1hSfdyAvnDZu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} T0 两个用户几乎同时发起扣减请求(毫秒级差距) T1 负载均衡分流,互不知情 T2 两个服务各自尝试加本地锁(几乎同时) ⚠ 两套 JVM 内存隔离,互不可见,各自都以为锁是空闲的 T3 两个服务同时进入临界区执行业务 par两个服务并行执行扣减 ❌ 同一份数据被两个进程同时操作,库存被重复扣减 请求扣减库存请求扣减库存路由到服务A路由到服务Bsynchronized 加锁 ✅ 成功synchronized 加锁 ✅ 成功读取库存 → 扣减 → 写回读取库存 → 扣减 → 写回

把这张图按时间轴拆开看就很清楚了:

  • T0:用户 A 和用户 B 几乎同时(毫秒级差距)发起扣减请求。
  • T1:负载均衡把两个请求分别路由到服务 A 和服务 B。
  • T2 :服务 A 在 JVM-A 内加 synchronized 锁成功;几乎同一时刻,服务 B 也在 JVM-B 内加 synchronized 锁成功。因为两套 JVM 的内存空间完全隔离,服务 B 根本看不到 JVM-A 上已经有一把锁,所以它视角里"锁是空闲的",加锁自然成功。
  • T3:两个服务同时进入临界区,都去读库存、扣减、写回,最终同一份数据被重复扣减。

关键点在于 T2 这一步:两个进程加锁的时间差极小,但由于锁状态分别存在各自的 JVM 内存里,彼此不可见,于是双双成功------这就是单机锁在多节点部署下失效的根本原因。

这就是单机锁失效的本质:

单机锁的可见性仅限于一个 JVM,而分布式部署下多个 JVM 之间没有共享的锁状态,导致互斥性被打破。

要解决这个问题,必须有一把所有节点都能看到的锁------这就是分布式锁。

1.3 什么是分布式锁

分布式锁的本质就是在分布式部署的环境下,实现一种跨进程、跨节点的互斥机制:锁只能被分布式系统中的某一个节点的某一个线程获取,其他节点、其他线程全部都得等待。

具体来说,一把合格的分布式锁需要满足:

  • 互斥性:任意时刻,只有一个客户端能持有锁。
  • 可重入性:同一个客户端(线程)在持有锁的情况下可以再次获取同一把锁,避免死锁。
  • 锁超时释放:持有锁的客户端如果崩溃或网络异常,锁最终要能被自动释放,避免死锁。
  • 高可用:锁服务本身要尽量可用,不能因为单个节点挂了导致整个加锁机制瘫痪。
  • 高性能:加锁、解锁要快,不能成为业务瓶颈。

1.4 为什么选 Redis

业内实现分布式锁有几种主流方案:数据库唯一索引、ZooKeeper、etcd、Redis。其中 Redis 是性能最高、使用最广的方案,原因如下:

  • 基于内存操作:所有读写都在内存中完成,加锁、解锁延迟极低(通常在毫秒级甚至微秒级),是 ZooKeeper、etcd 等基于磁盘或复杂协议的方案无法比的。
  • 核心命令单线程执行:Redis 的命令处理是单线程的(Redis 6.0 后网络 IO 多线程,但命令执行仍然单线程),自身不存在并发冲突,天然适合实现互斥操作。
  • 支持原子复合命令SET key value NX EX seconds 一条命令完成"查锁 + 加锁 + 设过期",避免窗口期。
  • 支持 Lua 脚本:复杂的"判断 + 删除"等操作可以用 Lua 脚本保证原子性。
  • 支持主从、哨兵、Cluster:自身具备高可用能力,可作为锁服务的基础设施。
  • 生态成熟:Redisson 等客户端已经把看门狗、可重入、RedLock 都封装好了,开箱即用。

性能和易用性兼具,让 Redis 成为分布式锁的事实标准。但 Redis 分布式锁并不是简单地 SETNX 一下就完事,它本身要解决一连串的问题。


二、Redis 分布式锁要解决的四大核心问题

把单机锁搬到 Redis 上,乍一看只要 SETNX 加锁、DEL 解锁就行了,但实际落地会暴露一连串问题。归纳下来,核心就是以下四个:

编号 问题 根因 后果
问题 1 锁争抢 查锁和加锁是两条命令,中间有窗口期 多个线程同时拿到锁,互斥失效
问题 2 僵尸锁 删锁失败,锁一直不释放 整个业务链路卡死,无法继续加锁
问题 3 锁过期 业务没执行完锁就超时释放 锁被别人拿走,且可能误删别人的锁
问题 4 锁存储 单 Redis 节点宕机,锁直接丢失 所有加解锁机制全部失效

下面按"问题 → 方案 → 新问题"的顺序逐步展开。


三、问题 1:锁争抢------查锁和加锁之间的窗口期

3.1 问题复现

最朴素的 Redis 分布式锁实现是两条命令:

bash 复制代码
# 1. 查锁
EXISTS lock_key
# 2. 加锁
SETNX lock_key unique_value

在拿锁之前先查一下锁是否存在,没人持有再 SETNX 加锁。问题在于:这两条命令之间有一个时间窗口
线程B Redis 线程A 线程B Redis 线程A #mermaid-svg-wNHp4LdrYv5JVk66{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wNHp4LdrYv5JVk66 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wNHp4LdrYv5JVk66 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wNHp4LdrYv5JVk66 .error-icon{fill:#552222;}#mermaid-svg-wNHp4LdrYv5JVk66 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wNHp4LdrYv5JVk66 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wNHp4LdrYv5JVk66 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wNHp4LdrYv5JVk66 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wNHp4LdrYv5JVk66 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wNHp4LdrYv5JVk66 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wNHp4LdrYv5JVk66 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wNHp4LdrYv5JVk66 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wNHp4LdrYv5JVk66 .marker.cross{stroke:#333333;}#mermaid-svg-wNHp4LdrYv5JVk66 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wNHp4LdrYv5JVk66 p{margin:0;}#mermaid-svg-wNHp4LdrYv5JVk66 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wNHp4LdrYv5JVk66 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-wNHp4LdrYv5JVk66 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wNHp4LdrYv5JVk66 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-wNHp4LdrYv5JVk66 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-wNHp4LdrYv5JVk66 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-wNHp4LdrYv5JVk66 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-wNHp4LdrYv5JVk66 .sequenceNumber{fill:white;}#mermaid-svg-wNHp4LdrYv5JVk66 #sequencenumber{fill:#333;}#mermaid-svg-wNHp4LdrYv5JVk66 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-wNHp4LdrYv5JVk66 .messageText{fill:#333;stroke:none;}#mermaid-svg-wNHp4LdrYv5JVk66 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wNHp4LdrYv5JVk66 .labelText,#mermaid-svg-wNHp4LdrYv5JVk66 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-wNHp4LdrYv5JVk66 .loopText,#mermaid-svg-wNHp4LdrYv5JVk66 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-wNHp4LdrYv5JVk66 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wNHp4LdrYv5JVk66 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-wNHp4LdrYv5JVk66 .noteText,#mermaid-svg-wNHp4LdrYv5JVk66 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-wNHp4LdrYv5JVk66 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wNHp4LdrYv5JVk66 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wNHp4LdrYv5JVk66 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wNHp4LdrYv5JVk66 .actorPopupMenu{position:absolute;}#mermaid-svg-wNHp4LdrYv5JVk66 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-wNHp4LdrYv5JVk66 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wNHp4LdrYv5JVk66 .actor-man circle,#mermaid-svg-wNHp4LdrYv5JVk66 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-wNHp4LdrYv5JVk66 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ⚠ 窗口期:A 还没加锁,B 进来了 ❌ 两个线程都拿到了锁 EXISTS lock_key0(锁不存在)EXISTS lock_key0(锁不存在)SETNX lock_key value_A1(加锁成功)SETNX lock_key value_B1(加锁成功)

线程 A 查完发现锁是空的,正准备加锁,线程 B 也来查,发现锁还是空的------于是两个线程都成功加锁,互斥性被打破。

3.2 解决方案:SET NX EX 原子加锁

Redis 提供了一条复合命令,把"查锁 + 加锁 + 设过期"合并成一次原子操作:

bash 复制代码
SET lock_key unique_value NX EX 30

参数解释:

  • NX(Not eXists) :仅当 lock_key 不存在时才执行加锁。这就把"判断"和"加锁"两条命令合并成一条,没有窗口期。
  • EX 30:同时设置 30 秒过期时间,作为僵尸锁兜底(问题 2 会用到)。
  • unique_value:锁的 value,必须是当前线程在分布式下的唯一标识(问题 3 防误删会用到)。

为什么这条命令是原子的?因为 Redis 的命令处理是单线程的,一条命令的执行过程中不会被其他命令打断。SET ... NX EX 是 Redis 2.6.12 之后支持的一条复合命令,等价于 SETNX + EXPIRE,但保证了原子性。

Java(Jedis)实现示例:

java 复制代码
String uniqueValue = UUID.randomUUID() + ":" + Thread.currentThread().getId();
// 一条命令完成:查锁 + 加锁 + 设过期
String result = jedis.set("lock_key", uniqueValue, SetParams.setParams().nx().ex(30));
if ("OK".equals(result)) {
    // 加锁成功
} else {
    // 加锁失败,锁已被其他人持有
}

到这里,问题 1 解决了。但新的问题马上来:锁加上了,万一释放失败怎么办?


四、问题 2:僵尸锁------删锁失败导致锁永不释放

4.1 问题复现

线程拿到了锁,执行完业务后,调用 DEL lock_key 释放锁。但这个 DEL 可能失败,导致锁一直不释放,整个业务再也无法加锁------这就是"僵尸锁"。

删锁失败一般有两类原因:

第一类:Redis 侧或网络侧问题

  • 网络抖动,DEL 的请求包丢在网络中。
  • Redis 连接超时(比如 Redis 正在 BGSAVE fork 子进程,主线程阻塞)。
  • Redis 节点临时不可达。

第二类:业务代码问题

  • 业务执行过程中抛出异常,代码中断,根本没有走到 DEL 这一步。
  • 业务抛了异常后虽然走到了 finally,但 finally 里的 DEL 又抛了新异常。

无论哪种情况,结果都是锁没被释放,下一次任何线程来加锁都会失败,整个被这把锁保护的业务链路彻底卡死。比如库存扣减服务,一旦锁成了僵尸锁,所有后续的扣减请求全部阻塞,业务直接瘫痪。

4.2 解决方案:重试 + try-catch-finally + TTL 兜底

针对两类原因,分别处理。

第一类(Redis/网络问题):重试 + 降级

针对 Redis 或网络侧的临时故障,采用重试机制

  • 设置最大重试次数,一般 2~3 次就够。
  • 每次重试之间间隔一小段时间(比如 100~200ms),不要立刻重试。因为故障可能是 Redis 正在 fork 子进程、网络拥塞等,需要给它一点恢复时间。
  • 重试仍然失败的话,把"释放锁"这个动作丢到消息队列里异步处理,借助 MQ 的重试机制兜底。

#mermaid-svg-DZW2MMfd4DPzwlQy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DZW2MMfd4DPzwlQy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DZW2MMfd4DPzwlQy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DZW2MMfd4DPzwlQy .error-icon{fill:#552222;}#mermaid-svg-DZW2MMfd4DPzwlQy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DZW2MMfd4DPzwlQy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DZW2MMfd4DPzwlQy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DZW2MMfd4DPzwlQy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DZW2MMfd4DPzwlQy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DZW2MMfd4DPzwlQy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DZW2MMfd4DPzwlQy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DZW2MMfd4DPzwlQy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DZW2MMfd4DPzwlQy .marker.cross{stroke:#333333;}#mermaid-svg-DZW2MMfd4DPzwlQy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DZW2MMfd4DPzwlQy p{margin:0;}#mermaid-svg-DZW2MMfd4DPzwlQy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DZW2MMfd4DPzwlQy .cluster-label text{fill:#333;}#mermaid-svg-DZW2MMfd4DPzwlQy .cluster-label span{color:#333;}#mermaid-svg-DZW2MMfd4DPzwlQy .cluster-label span p{background-color:transparent;}#mermaid-svg-DZW2MMfd4DPzwlQy .label text,#mermaid-svg-DZW2MMfd4DPzwlQy span{fill:#333;color:#333;}#mermaid-svg-DZW2MMfd4DPzwlQy .node rect,#mermaid-svg-DZW2MMfd4DPzwlQy .node circle,#mermaid-svg-DZW2MMfd4DPzwlQy .node ellipse,#mermaid-svg-DZW2MMfd4DPzwlQy .node polygon,#mermaid-svg-DZW2MMfd4DPzwlQy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DZW2MMfd4DPzwlQy .rough-node .label text,#mermaid-svg-DZW2MMfd4DPzwlQy .node .label text,#mermaid-svg-DZW2MMfd4DPzwlQy .image-shape .label,#mermaid-svg-DZW2MMfd4DPzwlQy .icon-shape .label{text-anchor:middle;}#mermaid-svg-DZW2MMfd4DPzwlQy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DZW2MMfd4DPzwlQy .rough-node .label,#mermaid-svg-DZW2MMfd4DPzwlQy .node .label,#mermaid-svg-DZW2MMfd4DPzwlQy .image-shape .label,#mermaid-svg-DZW2MMfd4DPzwlQy .icon-shape .label{text-align:center;}#mermaid-svg-DZW2MMfd4DPzwlQy .node.clickable{cursor:pointer;}#mermaid-svg-DZW2MMfd4DPzwlQy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DZW2MMfd4DPzwlQy .arrowheadPath{fill:#333333;}#mermaid-svg-DZW2MMfd4DPzwlQy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DZW2MMfd4DPzwlQy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DZW2MMfd4DPzwlQy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DZW2MMfd4DPzwlQy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DZW2MMfd4DPzwlQy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DZW2MMfd4DPzwlQy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DZW2MMfd4DPzwlQy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DZW2MMfd4DPzwlQy .cluster text{fill:#333;}#mermaid-svg-DZW2MMfd4DPzwlQy .cluster span{color:#333;}#mermaid-svg-DZW2MMfd4DPzwlQy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DZW2MMfd4DPzwlQy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DZW2MMfd4DPzwlQy rect.text{fill:none;stroke-width:0;}#mermaid-svg-DZW2MMfd4DPzwlQy .icon-shape,#mermaid-svg-DZW2MMfd4DPzwlQy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DZW2MMfd4DPzwlQy .icon-shape p,#mermaid-svg-DZW2MMfd4DPzwlQy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DZW2MMfd4DPzwlQy .icon-shape .label rect,#mermaid-svg-DZW2MMfd4DPzwlQy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DZW2MMfd4DPzwlQy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DZW2MMfd4DPzwlQy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DZW2MMfd4DPzwlQy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





业务执行完毕
尝试 DEL 释放锁
成功?
完成
重试第1次, 间隔 200ms
成功?
重试第2次, 间隔 200ms
成功?
投递到消息队列异步处理

第二类(代码问题):try-catch-finally

针对代码异常导致没走到 DEL,方案是把业务代码用 try-catch-finally 包起来,把释放锁的逻辑放到 finally 块里

java 复制代码
String uniqueValue = UUID.randomUUID() + ":" + Thread.currentThread().getId();
String result = jedis.set("lock_key", uniqueValue, SetParams.setParams().nx().ex(30));
if (!"OK".equals(result)) {
    return; // 加锁失败
}
try {
    // 业务逻辑(如扣减库存)
    doBusiness();
} catch (Exception e) {
    // 业务异常处理
    throw e;
} finally {
    // 无论业务是否抛异常, finally 都会执行
    // 注意: 这里用 Lua 脚本删锁防误删, 见问题 3
    releaseLockWithLua("lock_key", uniqueValue);
}

只要本地服务没崩,finally 里的删锁基本都能成功。

第三类(本地服务崩溃):TTL 兜底

如果本地服务整个崩了(比如 OOM、断电),那 finally 也救不了------进程都没了。这时只能依赖 Redis 端给锁设置的 TTL:进程崩了没关系,到了 30 秒锁会自动过期

这也是问题 1 中 SET ... EX 30 同时设过期时间的原因,TTL 是僵尸锁的最后一道兜底。

注意:这里默认已经解决了"误删别的线程的锁"的问题(用 Lua 脚本删锁,下文会讲)。否则 TTL 一过期,新线程拿到锁后,老线程的 finally 又来 DEL,会把新线程的锁删掉,引出问题 3。

4.3 TTL 的副作用:引出问题 3

到这里僵尸锁的问题解决了,但 TTL 又引出了新的问题:

既然设了 TTL,就可能出现"业务还没执行完,锁就过期了"的情况。

这就是问题 3------锁过期。


五、问题 3:锁过期------业务没完锁就没了

5.1 问题复现

锁过期会引发两个连锁问题:

问题 3.1:锁的时间不够业务处理

由于网络波动、GC 停顿、磁盘 IO 慢等原因,线程 A 的业务阻塞了一段时间,锁在业务执行完之前就超时释放了。线程 B 进来发现锁是空的,成功加锁,开始执行业务。这时线程 A 醒过来继续执行,导致两个线程同时在临界区操作共享数据,互斥性失效。

问题 3.2:误删别的线程的锁

接上面,线程 A 醒来后执行完业务,按惯例调用 DEL lock_key 释放锁------但它删掉的其实是线程 B 的锁(因为线程 A 的锁早就过期了,B 又重新加了一把)。

线程 C 又进来,发现锁被删了,再次成功加锁。线程 B 还在执行,于是线程 B 和线程 C 同时在临界区。系统彻底错乱:
线程C 线程B Redis 线程A 线程C 线程B Redis 线程A #mermaid-svg-CFDmUe9V10UU4YoX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CFDmUe9V10UU4YoX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CFDmUe9V10UU4YoX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CFDmUe9V10UU4YoX .error-icon{fill:#552222;}#mermaid-svg-CFDmUe9V10UU4YoX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CFDmUe9V10UU4YoX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CFDmUe9V10UU4YoX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CFDmUe9V10UU4YoX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CFDmUe9V10UU4YoX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CFDmUe9V10UU4YoX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CFDmUe9V10UU4YoX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CFDmUe9V10UU4YoX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CFDmUe9V10UU4YoX .marker.cross{stroke:#333333;}#mermaid-svg-CFDmUe9V10UU4YoX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CFDmUe9V10UU4YoX p{margin:0;}#mermaid-svg-CFDmUe9V10UU4YoX .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CFDmUe9V10UU4YoX text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-CFDmUe9V10UU4YoX .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-CFDmUe9V10UU4YoX .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-CFDmUe9V10UU4YoX .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-CFDmUe9V10UU4YoX .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-CFDmUe9V10UU4YoX #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-CFDmUe9V10UU4YoX .sequenceNumber{fill:white;}#mermaid-svg-CFDmUe9V10UU4YoX #sequencenumber{fill:#333;}#mermaid-svg-CFDmUe9V10UU4YoX #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-CFDmUe9V10UU4YoX .messageText{fill:#333;stroke:none;}#mermaid-svg-CFDmUe9V10UU4YoX .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CFDmUe9V10UU4YoX .labelText,#mermaid-svg-CFDmUe9V10UU4YoX .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-CFDmUe9V10UU4YoX .loopText,#mermaid-svg-CFDmUe9V10UU4YoX .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-CFDmUe9V10UU4YoX .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-CFDmUe9V10UU4YoX .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-CFDmUe9V10UU4YoX .noteText,#mermaid-svg-CFDmUe9V10UU4YoX .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-CFDmUe9V10UU4YoX .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CFDmUe9V10UU4YoX .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CFDmUe9V10UU4YoX .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CFDmUe9V10UU4YoX .actorPopupMenu{position:absolute;}#mermaid-svg-CFDmUe9V10UU4YoX .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-CFDmUe9V10UU4YoX .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CFDmUe9V10UU4YoX .actor-man circle,#mermaid-svg-CFDmUe9V10UU4YoX line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-CFDmUe9V10UU4YoX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务阻塞 40s(GC、网络等) 线程B 开始执行业务 线程A 醒来, 继续执行 ❌ B 和 C 同时在临界区, 数据错乱 加锁成功, TTL=30s30s 到, 锁自动过期加锁成功(锁已过期)DEL lock_key(误删了B的锁!)加锁成功(锁又被删了)

所以问题 3 要解决两件事:

  1. 锁的时间不够:让锁能跟着业务执行时间动态续期。
  2. 误删别的线程的锁:让锁只属于当前线程,删除前先校验。

5.2 解决方案一:看门狗机制(解决锁续期)

5.2.1 看门狗原理

大名鼎鼎的"看门狗(Watchdog)"机制就是为锁续期而生的。核心思路:
加锁成功后,在后台开启一个独立线程(看门狗),定时检查业务是否还在执行。如果业务还没结束,就给锁续期(重新 EXPIRE),避免锁提前过期。
#mermaid-svg-iomRTvJZDCJIvLjl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iomRTvJZDCJIvLjl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iomRTvJZDCJIvLjl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iomRTvJZDCJIvLjl .error-icon{fill:#552222;}#mermaid-svg-iomRTvJZDCJIvLjl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iomRTvJZDCJIvLjl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iomRTvJZDCJIvLjl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iomRTvJZDCJIvLjl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iomRTvJZDCJIvLjl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iomRTvJZDCJIvLjl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iomRTvJZDCJIvLjl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iomRTvJZDCJIvLjl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iomRTvJZDCJIvLjl .marker.cross{stroke:#333333;}#mermaid-svg-iomRTvJZDCJIvLjl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iomRTvJZDCJIvLjl p{margin:0;}#mermaid-svg-iomRTvJZDCJIvLjl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iomRTvJZDCJIvLjl .cluster-label text{fill:#333;}#mermaid-svg-iomRTvJZDCJIvLjl .cluster-label span{color:#333;}#mermaid-svg-iomRTvJZDCJIvLjl .cluster-label span p{background-color:transparent;}#mermaid-svg-iomRTvJZDCJIvLjl .label text,#mermaid-svg-iomRTvJZDCJIvLjl span{fill:#333;color:#333;}#mermaid-svg-iomRTvJZDCJIvLjl .node rect,#mermaid-svg-iomRTvJZDCJIvLjl .node circle,#mermaid-svg-iomRTvJZDCJIvLjl .node ellipse,#mermaid-svg-iomRTvJZDCJIvLjl .node polygon,#mermaid-svg-iomRTvJZDCJIvLjl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iomRTvJZDCJIvLjl .rough-node .label text,#mermaid-svg-iomRTvJZDCJIvLjl .node .label text,#mermaid-svg-iomRTvJZDCJIvLjl .image-shape .label,#mermaid-svg-iomRTvJZDCJIvLjl .icon-shape .label{text-anchor:middle;}#mermaid-svg-iomRTvJZDCJIvLjl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iomRTvJZDCJIvLjl .rough-node .label,#mermaid-svg-iomRTvJZDCJIvLjl .node .label,#mermaid-svg-iomRTvJZDCJIvLjl .image-shape .label,#mermaid-svg-iomRTvJZDCJIvLjl .icon-shape .label{text-align:center;}#mermaid-svg-iomRTvJZDCJIvLjl .node.clickable{cursor:pointer;}#mermaid-svg-iomRTvJZDCJIvLjl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iomRTvJZDCJIvLjl .arrowheadPath{fill:#333333;}#mermaid-svg-iomRTvJZDCJIvLjl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iomRTvJZDCJIvLjl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iomRTvJZDCJIvLjl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iomRTvJZDCJIvLjl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iomRTvJZDCJIvLjl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iomRTvJZDCJIvLjl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iomRTvJZDCJIvLjl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iomRTvJZDCJIvLjl .cluster text{fill:#333;}#mermaid-svg-iomRTvJZDCJIvLjl .cluster span{color:#333;}#mermaid-svg-iomRTvJZDCJIvLjl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iomRTvJZDCJIvLjl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iomRTvJZDCJIvLjl rect.text{fill:none;stroke-width:0;}#mermaid-svg-iomRTvJZDCJIvLjl .icon-shape,#mermaid-svg-iomRTvJZDCJIvLjl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iomRTvJZDCJIvLjl .icon-shape p,#mermaid-svg-iomRTvJZDCJIvLjl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iomRTvJZDCJIvLjl .icon-shape .label rect,#mermaid-svg-iomRTvJZDCJIvLjl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iomRTvJZDCJIvLjl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iomRTvJZDCJIvLjl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iomRTvJZDCJIvLjl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 看门狗线程
业务线程


触发
通知停止
加锁成功
执行业务逻辑
业务完成, 释放锁
启动定时任务
业务是否完成?
给锁续期 EXPIRE
停止看门狗

以 Redisson 的实现为例,看门狗的关键参数:

  • lockWatchdogTimeout:默认 30 秒,作为锁的初始过期时间。
  • 看门狗每 lockWatchdogTimeout / 3 = 10 秒 执行一次续期,把锁的过期时间重置为 30 秒。
  • 业务执行完成后,调用 unlock() 会同时停止看门狗。
java 复制代码
// Redisson 看门狗生效的前提:不指定 leaseTime
RLock lock = redisson.getLock("lock_key");
lock.lock(); // 不传 leaseTime, 看门狗自动启动
try {
    // 业务逻辑, 即使执行超过 30s 也不会丢锁
    doBusiness();
} finally {
    lock.unlock(); // 释放锁并停止看门狗
}
5.2.2 看门狗的两个边界情况

情况一:业务一直无法完成

如果业务因为 bug、死循环、依赖故障等原因一直无法结束,看门狗就会无限续期,锁永远不释放。这显然不合理。

解决方案是设置一个兜底的最大续期时间(比如 5 分钟),超过这个时间还认为业务没完成,就直接抛异常、停止续期、告警通知运维介入修复,避免无限续期。

情况二:本地服务崩溃

这是看门狗无法解决的场景。看门狗线程和业务线程跑在同一个 JVM 里,一旦本地服务宕机(OOM、断电、进程被 kill),看门狗线程也跟着死了,没法再续期。锁到了 TTL 自动释放。

但这时危险在于:

线程 A 已经操作了一部分数据(比如扣了库存但还没写日志),还没来得及回滚,服务就崩了。锁释放后,线程 B 进来继续操作这"半截数据",最终数据彻底错乱,无法回滚,造成重大事故。

5.2.3 看门狗之外的补充方案

既然看门狗解决不了崩溃场景,就要从其他角度做兜底:

  1. 业务设计要"短平快":尽量不要让分布式锁保护的业务执行时间过长,让业务执行时间远小于锁的 TTL。把耗时的操作(比如发短信、调外部接口)挪到锁外面异步执行。
  2. 做好业务幂等性:依靠幂等机制防止重复处理。即使锁失效导致同一请求被执行两次,结果也是一致的。
  3. 处理中标记:重要业务在拿到锁后,先在共享存储(数据库或 Redis)里把状态标记为"处理中"。即使本地崩了,其他线程进来发现状态是"处理中"也不会贸然处理,需要人工介入或走补偿流程。
  4. 数据库最终兜底:对于库存这种关键数据,数据库层面也要有乐观锁/版本号兜底,不能完全依赖分布式锁。

5.3 解决方案二:UUID + 线程 ID + Lua 脚本(解决误删锁)

5.3.1 value 设计成全局唯一

要防止线程 A 误删线程 B 的锁,就要让锁只认"主人"。这就用到加锁命令里那个 unique_value

value 必须在分布式环境下唯一标识当前线程。

业内通用做法是 UUID + 线程 ID

java 复制代码
// UUID 保证跨 JVM 唯一, 线程 ID 保证同一 JVM 内不同线程唯一
String uniqueValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();

加锁时把 unique_value 存进去,删锁时先比对 value 是不是自己的。

5.3.2 删锁必须用 Lua 脚本

光比对 value 还不够,因为"查 value + 删 key"又是两条命令,中间又有窗口期:
线程B Redis 线程A 线程B Redis 线程A #mermaid-svg-1choFvFCeBcrbm5n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1choFvFCeBcrbm5n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1choFvFCeBcrbm5n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1choFvFCeBcrbm5n .error-icon{fill:#552222;}#mermaid-svg-1choFvFCeBcrbm5n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1choFvFCeBcrbm5n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1choFvFCeBcrbm5n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1choFvFCeBcrbm5n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1choFvFCeBcrbm5n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1choFvFCeBcrbm5n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1choFvFCeBcrbm5n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1choFvFCeBcrbm5n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1choFvFCeBcrbm5n .marker.cross{stroke:#333333;}#mermaid-svg-1choFvFCeBcrbm5n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1choFvFCeBcrbm5n p{margin:0;}#mermaid-svg-1choFvFCeBcrbm5n .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1choFvFCeBcrbm5n text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-1choFvFCeBcrbm5n .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1choFvFCeBcrbm5n .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-1choFvFCeBcrbm5n .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-1choFvFCeBcrbm5n .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-1choFvFCeBcrbm5n #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-1choFvFCeBcrbm5n .sequenceNumber{fill:white;}#mermaid-svg-1choFvFCeBcrbm5n #sequencenumber{fill:#333;}#mermaid-svg-1choFvFCeBcrbm5n #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-1choFvFCeBcrbm5n .messageText{fill:#333;stroke:none;}#mermaid-svg-1choFvFCeBcrbm5n .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1choFvFCeBcrbm5n .labelText,#mermaid-svg-1choFvFCeBcrbm5n .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-1choFvFCeBcrbm5n .loopText,#mermaid-svg-1choFvFCeBcrbm5n .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-1choFvFCeBcrbm5n .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1choFvFCeBcrbm5n .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-1choFvFCeBcrbm5n .noteText,#mermaid-svg-1choFvFCeBcrbm5n .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-1choFvFCeBcrbm5n .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1choFvFCeBcrbm5n .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1choFvFCeBcrbm5n .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1choFvFCeBcrbm5n .actorPopupMenu{position:absolute;}#mermaid-svg-1choFvFCeBcrbm5n .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-1choFvFCeBcrbm5n .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1choFvFCeBcrbm5n .actor-man circle,#mermaid-svg-1choFvFCeBcrbm5n line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-1choFvFCeBcrbm5n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ⚠ 窗口期:A 的锁正好过期, B 加锁成功 GET lock_keyvalue_A (是自己的)A 的锁过期加锁成功, value=value_BDEL lock_key (删了B的锁!)

要消除这个窗口期,必须把"判断 value + 删除 key"做成原子操作。Redis 的 Lua 脚本天生支持原子性(脚本执行期间不会被其他命令打断):

lua 复制代码
-- release_lock.lua
-- KEYS[1] = lock_key
-- ARGV[1] = unique_value
if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
else
    return 0
end

Java 调用示例:

java 复制代码
String luaScript =
    "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
    "  return redis.call('DEL', KEYS[1]) " +
    "else " +
    "  return 0 " +
    "end";
Object result = jedis.eval(
    luaScript,
    Collections.singletonList("lock_key"),
    Collections.singletonList(uniqueValue)
);

这样"判断 + 删除"在 Redis 内是一个原子操作,彻底解决了误删问题。问题 3 至此闭环。

5.4 问题 3 小结

子问题 方案 关键点
锁时间不够 看门狗续期 后台线程定时 EXPIRE,Redisson 默认每 10s 续到 30s
看门狗续到天荒地老 最大续期时间兜底 超时抛异常 + 告警
看门狗解决不了崩溃 业务短平快 + 幂等 + 处理中标记 + DB 兜底 多重防线
误删别的线程的锁 UUID+线程ID 做 value + Lua 脚本删锁 判断和删除原子化

六、问题 4:锁存储------单节点 Redis 宕机锁丢失

6.1 问题复现

前面所有方案都基于一个隐含假设:Redis 一直可用。但实际上 Redis 也是会挂的。

如果只有一个 Redis 节点存锁,一旦这个节点宕机:

  • 已经加的锁全部丢失。
  • 后续所有加锁、解锁、查锁的请求全部失败。
  • 整个分布式锁机制瘫痪。

有人会说:给 Redis 配主从+哨兵不就行了?但这里有个致命的同步间隙

Redis 主从复制是异步的。主节点加锁成功后还没来得及同步给从节点就挂了,哨兵把从节点提升为主节点,但新主节点上没有这把锁------锁丢了。

于是两个客户端可以同时加锁成功,互斥性失效。这就是问题 4 的核心。

6.2 解决方案:RedLock 算法

Redis 作者 antirez 针对这个问题提出了 RedLock(Red lock)算法 。核心思想是不依赖单个 Redis 节点,而是同时向多个独立的 Redis 节点申请锁

6.2.1 算法流程

部署至少 5 个(一般推荐 5 个,最少 3 个)相互独立的 Redis 实例(不是主从、不是 Cluster),客户端依次向每个节点申请同一把锁:
#mermaid-svg-RAgZoVvwPr56lESe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RAgZoVvwPr56lESe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RAgZoVvwPr56lESe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RAgZoVvwPr56lESe .error-icon{fill:#552222;}#mermaid-svg-RAgZoVvwPr56lESe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RAgZoVvwPr56lESe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RAgZoVvwPr56lESe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RAgZoVvwPr56lESe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RAgZoVvwPr56lESe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RAgZoVvwPr56lESe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RAgZoVvwPr56lESe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RAgZoVvwPr56lESe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RAgZoVvwPr56lESe .marker.cross{stroke:#333333;}#mermaid-svg-RAgZoVvwPr56lESe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RAgZoVvwPr56lESe p{margin:0;}#mermaid-svg-RAgZoVvwPr56lESe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RAgZoVvwPr56lESe .cluster-label text{fill:#333;}#mermaid-svg-RAgZoVvwPr56lESe .cluster-label span{color:#333;}#mermaid-svg-RAgZoVvwPr56lESe .cluster-label span p{background-color:transparent;}#mermaid-svg-RAgZoVvwPr56lESe .label text,#mermaid-svg-RAgZoVvwPr56lESe span{fill:#333;color:#333;}#mermaid-svg-RAgZoVvwPr56lESe .node rect,#mermaid-svg-RAgZoVvwPr56lESe .node circle,#mermaid-svg-RAgZoVvwPr56lESe .node ellipse,#mermaid-svg-RAgZoVvwPr56lESe .node polygon,#mermaid-svg-RAgZoVvwPr56lESe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RAgZoVvwPr56lESe .rough-node .label text,#mermaid-svg-RAgZoVvwPr56lESe .node .label text,#mermaid-svg-RAgZoVvwPr56lESe .image-shape .label,#mermaid-svg-RAgZoVvwPr56lESe .icon-shape .label{text-anchor:middle;}#mermaid-svg-RAgZoVvwPr56lESe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RAgZoVvwPr56lESe .rough-node .label,#mermaid-svg-RAgZoVvwPr56lESe .node .label,#mermaid-svg-RAgZoVvwPr56lESe .image-shape .label,#mermaid-svg-RAgZoVvwPr56lESe .icon-shape .label{text-align:center;}#mermaid-svg-RAgZoVvwPr56lESe .node.clickable{cursor:pointer;}#mermaid-svg-RAgZoVvwPr56lESe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RAgZoVvwPr56lESe .arrowheadPath{fill:#333333;}#mermaid-svg-RAgZoVvwPr56lESe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RAgZoVvwPr56lESe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RAgZoVvwPr56lESe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RAgZoVvwPr56lESe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RAgZoVvwPr56lESe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RAgZoVvwPr56lESe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RAgZoVvwPr56lESe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RAgZoVvwPr56lESe .cluster text{fill:#333;}#mermaid-svg-RAgZoVvwPr56lESe .cluster span{color:#333;}#mermaid-svg-RAgZoVvwPr56lESe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RAgZoVvwPr56lESe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RAgZoVvwPr56lESe rect.text{fill:none;stroke-width:0;}#mermaid-svg-RAgZoVvwPr56lESe .icon-shape,#mermaid-svg-RAgZoVvwPr56lESe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RAgZoVvwPr56lESe .icon-shape p,#mermaid-svg-RAgZoVvwPr56lESe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RAgZoVvwPr56lESe .icon-shape .label rect,#mermaid-svg-RAgZoVvwPr56lESe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RAgZoVvwPr56lESe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RAgZoVvwPr56lESe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RAgZoVvwPr56lESe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

客户端
Redis节点1

加锁成功
Redis节点2

加锁成功
Redis节点3

加锁成功
Redis节点4

加锁失败
Redis节点5

加锁失败
成功节点数 >= 3?
加锁成功
加锁失败, 向所有节点释放锁

完整步骤:

  1. 客户端记录当前时间戳 T1(毫秒)。
  2. 依次向 N 个 Redis 节点发送加锁请求(SET lock_key unique_value NX EX TTL),每个请求都设置一个很短的连接超时时间(比如 50~100ms),避免某个节点卡死拖慢整体。
  3. 统计成功加锁的节点数:
    • 如果成功数 >= N/2 + 1(半数以上) ,且总耗时 < TTL(锁还有效),则认为加锁成功,锁的实际有效时间为 TTL - (T2 - T1)
    • 否则加锁失败,向所有节点(包括没加成功的)发送 DEL 释放锁,让其他客户端能尽快拿到锁。
  4. 业务执行完后,向所有节点发送释放锁请求。
6.2.2 部署要求

RedLock 的几个关键约束:

  • 节点相互独立:不能是主从关系,也不能是 Cluster 节点,必须各自独立的 Redis 实例。
  • 跨机房部署:最好分布在不同机房,避免机房级故障。
  • 设置合理的加锁超时:单节点加锁请求的超时一般设为 50~100ms,避免某节点网络抖动卡死整个流程。
  • TTL 要大于锁的有效窗口:要预留足够时间让业务执行。

6.3 RedLock 的问题与争议

RedLock 看似完美,但实际有不少争议和局限:

问题 1:性能损耗

加锁和释放锁都要向 N 个节点发请求,任何几个节点网络抖动都会拖慢整体效率。相比单节点 Redis 锁,性能下降明显。

问题 2:高并发下成功率低

线程冲突激烈时,要同时抢到半数以上节点的锁很难,可能多次重试都失败,导致服务"半天不动,全在抢锁"。

问题 3:GC 停顿/时钟漂移争议

分布式系统专家 Martin Kleppmann 曾撰文质疑 RedLock:客户端发生长 GC STW 时,可能"自以为还持有锁",实际上锁早已过期被别人拿走,依然会破坏互斥性。RedLock 依赖各节点时钟一致,但分布式系统时钟漂移是客观存在的。这场争论至今没有定论。

实践建议

  • 对正确性要求极高的场景(比如金融扣款),建议用 ZooKeeper/etcd 这类基于共识算法的方案,或者直接走数据库事务。
  • 对性能要求高、能容忍极端情况下偶尔失效的场景,单 Redis + 看门狗已经足够,没必要上 RedLock。
  • 真要上 RedLock,直接用 Redisson 的 RedissonRedLock,别手搓。

七、被忽略的关键问题:锁粒度

讲了这么多机制,还有一个容易被忽略却极其关键的问题------锁的粒度,也就是锁的范围。

7.1 粒度的影响

锁粒度直接决定并发能力:

  • 粒度太粗:比如锁住整个商品服务的所有业务,所有商品操作串行执行,并发能力极低。
  • 粒度太细:比如按 SKU 维度加锁,并发能力高,但复杂度上升,还可能出现"锁爆炸"。

7.2 锁爆炸

锁爆炸常见于树形/继承树结构的数据。比如商品分类树:
#mermaid-svg-XMR8JJd3hJMIks2i{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XMR8JJd3hJMIks2i .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XMR8JJd3hJMIks2i .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XMR8JJd3hJMIks2i .error-icon{fill:#552222;}#mermaid-svg-XMR8JJd3hJMIks2i .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XMR8JJd3hJMIks2i .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XMR8JJd3hJMIks2i .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XMR8JJd3hJMIks2i .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XMR8JJd3hJMIks2i .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XMR8JJd3hJMIks2i .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XMR8JJd3hJMIks2i .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XMR8JJd3hJMIks2i .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XMR8JJd3hJMIks2i .marker.cross{stroke:#333333;}#mermaid-svg-XMR8JJd3hJMIks2i svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XMR8JJd3hJMIks2i p{margin:0;}#mermaid-svg-XMR8JJd3hJMIks2i .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XMR8JJd3hJMIks2i .cluster-label text{fill:#333;}#mermaid-svg-XMR8JJd3hJMIks2i .cluster-label span{color:#333;}#mermaid-svg-XMR8JJd3hJMIks2i .cluster-label span p{background-color:transparent;}#mermaid-svg-XMR8JJd3hJMIks2i .label text,#mermaid-svg-XMR8JJd3hJMIks2i span{fill:#333;color:#333;}#mermaid-svg-XMR8JJd3hJMIks2i .node rect,#mermaid-svg-XMR8JJd3hJMIks2i .node circle,#mermaid-svg-XMR8JJd3hJMIks2i .node ellipse,#mermaid-svg-XMR8JJd3hJMIks2i .node polygon,#mermaid-svg-XMR8JJd3hJMIks2i .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XMR8JJd3hJMIks2i .rough-node .label text,#mermaid-svg-XMR8JJd3hJMIks2i .node .label text,#mermaid-svg-XMR8JJd3hJMIks2i .image-shape .label,#mermaid-svg-XMR8JJd3hJMIks2i .icon-shape .label{text-anchor:middle;}#mermaid-svg-XMR8JJd3hJMIks2i .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XMR8JJd3hJMIks2i .rough-node .label,#mermaid-svg-XMR8JJd3hJMIks2i .node .label,#mermaid-svg-XMR8JJd3hJMIks2i .image-shape .label,#mermaid-svg-XMR8JJd3hJMIks2i .icon-shape .label{text-align:center;}#mermaid-svg-XMR8JJd3hJMIks2i .node.clickable{cursor:pointer;}#mermaid-svg-XMR8JJd3hJMIks2i .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XMR8JJd3hJMIks2i .arrowheadPath{fill:#333333;}#mermaid-svg-XMR8JJd3hJMIks2i .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XMR8JJd3hJMIks2i .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XMR8JJd3hJMIks2i .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XMR8JJd3hJMIks2i .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XMR8JJd3hJMIks2i .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XMR8JJd3hJMIks2i .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XMR8JJd3hJMIks2i .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XMR8JJd3hJMIks2i .cluster text{fill:#333;}#mermaid-svg-XMR8JJd3hJMIks2i .cluster span{color:#333;}#mermaid-svg-XMR8JJd3hJMIks2i div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XMR8JJd3hJMIks2i .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XMR8JJd3hJMIks2i rect.text{fill:none;stroke-width:0;}#mermaid-svg-XMR8JJd3hJMIks2i .icon-shape,#mermaid-svg-XMR8JJd3hJMIks2i .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XMR8JJd3hJMIks2i .icon-shape p,#mermaid-svg-XMR8JJd3hJMIks2i .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XMR8JJd3hJMIks2i .icon-shape .label rect,#mermaid-svg-XMR8JJd3hJMIks2i .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XMR8JJd3hJMIks2i .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XMR8JJd3hJMIks2i .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XMR8JJd3hJMIks2i :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 根分类
分类A
分类B
子分类A1
子分类A2
子分类B1
子分类B2
叶子A1a
叶子A1b

如果操作某个子分类时要锁住它所有的子节点(防止并发修改子树),你就要加 N 把锁。锁释放时只要有一把失败,整个子树都被拖住,连锁反应下问题被放大。

这种场景下,与其细粒度锁每个节点,不如直接锁住整棵子树

  • 加一把锁,逻辑简单,出问题概率低。
  • 即使释放失败,TTL 兜底也只需要等一把锁的过期。
  • 实现成本低,运维成本低。

7.3 选择粒度的原则

  • 根据业务场景选择合适的粒度,不要为了细而细
  • 高并发热点数据用细粒度锁(比如按 SKU ID 加锁)。
  • 强关联、树形结构用粗粒度锁(整个子树一把锁)。
  • 不滥用分布式锁,能用本地锁解决的不要上分布式锁。

八、生产实践:用 Redisson,别手搓

前面所有的问题和方案,看起来每一步都不复杂,但真要把 SET NX EX、UUID、Lua、看门狗、重试、TTL 兜底全部正确组合到一起,工作量极大,而且很容易踩坑(比如看门狗线程泄漏、Lua 脚本写错、释放锁时连接断开等)。

Redisson 已经把这一切都封装好了

  • 可重入锁:基于 Hash 结构记录重入次数。
  • 看门狗lock.lock() 不传 leaseTime 时自动启用,每 10s 续期到 30s。
  • 公平锁、读写锁、信号量:一应俱全。
  • RedLockRedissonRedLock 直接可用。
  • Lua 脚本:所有加锁、解锁、续期内部都走 Lua,保证原子性。
java 复制代码
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

RLock lock = redisson.getLock("order_lock:1001");
// 不传 leaseTime, 看门狗自动续期
lock.lock();
try {
    // 业务逻辑
    doBusiness();
} finally {
    // 内部用 Lua 脚本判断 value + 删锁, 不会误删
    lock.unlock();
}

生产环境强烈建议直接用 Redisson,不要手搓分布式锁。把精力放在业务设计和锁粒度选择上,比重复造轮子有价值得多。


九、总结:Redis 分布式锁演进全景

用一张图把整条演进线串起来:
#mermaid-svg-6nSjfD21X0E7aPn1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6nSjfD21X0E7aPn1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6nSjfD21X0E7aPn1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6nSjfD21X0E7aPn1 .error-icon{fill:#552222;}#mermaid-svg-6nSjfD21X0E7aPn1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6nSjfD21X0E7aPn1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6nSjfD21X0E7aPn1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6nSjfD21X0E7aPn1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6nSjfD21X0E7aPn1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6nSjfD21X0E7aPn1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6nSjfD21X0E7aPn1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6nSjfD21X0E7aPn1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6nSjfD21X0E7aPn1 .marker.cross{stroke:#333333;}#mermaid-svg-6nSjfD21X0E7aPn1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6nSjfD21X0E7aPn1 p{margin:0;}#mermaid-svg-6nSjfD21X0E7aPn1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6nSjfD21X0E7aPn1 .cluster-label text{fill:#333;}#mermaid-svg-6nSjfD21X0E7aPn1 .cluster-label span{color:#333;}#mermaid-svg-6nSjfD21X0E7aPn1 .cluster-label span p{background-color:transparent;}#mermaid-svg-6nSjfD21X0E7aPn1 .label text,#mermaid-svg-6nSjfD21X0E7aPn1 span{fill:#333;color:#333;}#mermaid-svg-6nSjfD21X0E7aPn1 .node rect,#mermaid-svg-6nSjfD21X0E7aPn1 .node circle,#mermaid-svg-6nSjfD21X0E7aPn1 .node ellipse,#mermaid-svg-6nSjfD21X0E7aPn1 .node polygon,#mermaid-svg-6nSjfD21X0E7aPn1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6nSjfD21X0E7aPn1 .rough-node .label text,#mermaid-svg-6nSjfD21X0E7aPn1 .node .label text,#mermaid-svg-6nSjfD21X0E7aPn1 .image-shape .label,#mermaid-svg-6nSjfD21X0E7aPn1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-6nSjfD21X0E7aPn1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6nSjfD21X0E7aPn1 .rough-node .label,#mermaid-svg-6nSjfD21X0E7aPn1 .node .label,#mermaid-svg-6nSjfD21X0E7aPn1 .image-shape .label,#mermaid-svg-6nSjfD21X0E7aPn1 .icon-shape .label{text-align:center;}#mermaid-svg-6nSjfD21X0E7aPn1 .node.clickable{cursor:pointer;}#mermaid-svg-6nSjfD21X0E7aPn1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6nSjfD21X0E7aPn1 .arrowheadPath{fill:#333333;}#mermaid-svg-6nSjfD21X0E7aPn1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6nSjfD21X0E7aPn1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6nSjfD21X0E7aPn1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6nSjfD21X0E7aPn1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6nSjfD21X0E7aPn1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6nSjfD21X0E7aPn1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6nSjfD21X0E7aPn1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6nSjfD21X0E7aPn1 .cluster text{fill:#333;}#mermaid-svg-6nSjfD21X0E7aPn1 .cluster span{color:#333;}#mermaid-svg-6nSjfD21X0E7aPn1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6nSjfD21X0E7aPn1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6nSjfD21X0E7aPn1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-6nSjfD21X0E7aPn1 .icon-shape,#mermaid-svg-6nSjfD21X0E7aPn1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6nSjfD21X0E7aPn1 .icon-shape p,#mermaid-svg-6nSjfD21X0E7aPn1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6nSjfD21X0E7aPn1 .icon-shape .label rect,#mermaid-svg-6nSjfD21X0E7aPn1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6nSjfD21X0E7aPn1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6nSjfD21X0E7aPn1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6nSjfD21X0E7aPn1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
Java 单机锁

synchronized / ReentrantLock
多节点部署?
锁失效, 互斥被打破
引入 Redis 分布式锁
问题1: 锁争抢

查锁和加锁之间有窗口期
方案: SET NX EX 原子加锁
问题2: 僵尸锁

删锁失败锁不释放
方案: 重试 + try-finally + TTL 兜底
问题3: 锁过期

业务没完锁就没了
方案1: 看门狗续期
方案2: UUID+线程ID+Lua 防误删
看门狗解决不了崩溃
方案3: 业务短平快 + 幂等 + 处理中标记
问题4: 锁存储

单节点宕机锁丢失
方案: RedLock 多节点加锁
最终: Redisson 一站式封装

整条链路的核心思路:

  1. 单机锁失效 → 引入 Redis 分布式锁,让所有节点共享锁状态。
  2. 锁争抢SET NX EX 原子加锁,消除查锁和加锁之间的窗口期。
  3. 僵尸锁 → 重试 + try-catch-finally + TTL 三重兜底。
  4. 锁过期 → 看门狗动态续期,UUID + Lua 防误删,业务幂等兜底崩溃场景。
  5. 单节点故障 → RedLock 多节点半数加锁,提升锁服务高可用。
  6. 生产落地 → 直接用 Redisson,不重复造轮子。

最后再强调一句:没有银弹。Redis 分布式锁在极端场景下(GC STW、时钟漂移、看门狗崩溃)依然有失效的可能,对于正确性要求极高的业务,必须配合业务幂等、数据库乐观锁、状态机校验等多重防线,而不是把所有宝都压在分布式锁上。

相关推荐
数智化管理手记2 小时前
智能财务如何减少财务加班?智能财务落地需要哪些工具支撑?
大数据·网络·数据库·数据挖掘·精益工程
神奇小汤圆3 小时前
雪花算法ID重复了?惨痛教训:请勿轻易造轮子!
后端
IvorySQL3 小时前
深度拆解 IvorySQL 去 O 核心解决方案
数据库·人工智能·postgresql
Java内核笔记3 小时前
万字避坑!Spring Boot 3.x 升 4.0 最全升级指南(附 Migration Checklist,建议收藏⭐)
java·后端
从零开始的代码生活_3 小时前
C++ list 原理与实践:双向链表、迭代器与简化实现
开发语言·c++·后端·学习·算法·链表·list
初学AI的小高3 小时前
FastAPI 工程实践:LLM 应用的 API 设计之道
后端·fastapi
A15362553 小时前
国内好用的WMS仓储管理系统有哪些?万里牛WMS深度评测
大数据·数据库·人工智能
SelectDB3 小时前
快手从 ClickHouse 到 Apache Doris 的百 PB 数据、200+集群迁移实践
数据库·性能优化·开源
衣乌安、3 小时前
Redis 分布式锁与任务状态
数据库·redis·分布式