Java 的
synchronized和ReentrantLock只能锁住单个 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 正在
BGSAVEfork 子进程,主线程阻塞)。 - 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 要解决两件事:
- 锁的时间不够:让锁能跟着业务执行时间动态续期。
- 误删别的线程的锁:让锁只属于当前线程,删除前先校验。
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 看门狗之外的补充方案
既然看门狗解决不了崩溃场景,就要从其他角度做兜底:
- 业务设计要"短平快":尽量不要让分布式锁保护的业务执行时间过长,让业务执行时间远小于锁的 TTL。把耗时的操作(比如发短信、调外部接口)挪到锁外面异步执行。
- 做好业务幂等性:依靠幂等机制防止重复处理。即使锁失效导致同一请求被执行两次,结果也是一致的。
- 处理中标记:重要业务在拿到锁后,先在共享存储(数据库或 Redis)里把状态标记为"处理中"。即使本地崩了,其他线程进来发现状态是"处理中"也不会贸然处理,需要人工介入或走补偿流程。
- 数据库最终兜底:对于库存这种关键数据,数据库层面也要有乐观锁/版本号兜底,不能完全依赖分布式锁。
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?
加锁成功
加锁失败, 向所有节点释放锁
完整步骤:
- 客户端记录当前时间戳
T1(毫秒)。 - 依次向 N 个 Redis 节点发送加锁请求(
SET lock_key unique_value NX EX TTL),每个请求都设置一个很短的连接超时时间(比如 50~100ms),避免某个节点卡死拖慢整体。 - 统计成功加锁的节点数:
- 如果成功数 >=
N/2 + 1(半数以上) ,且总耗时< TTL(锁还有效),则认为加锁成功,锁的实际有效时间为TTL - (T2 - T1)。 - 否则加锁失败,向所有节点(包括没加成功的)发送
DEL释放锁,让其他客户端能尽快拿到锁。
- 如果成功数 >=
- 业务执行完后,向所有节点发送释放锁请求。
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。 - 公平锁、读写锁、信号量:一应俱全。
- RedLock :
RedissonRedLock直接可用。 - 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 一站式封装
整条链路的核心思路:
- 单机锁失效 → 引入 Redis 分布式锁,让所有节点共享锁状态。
- 锁争抢 →
SET NX EX原子加锁,消除查锁和加锁之间的窗口期。 - 僵尸锁 → 重试 +
try-catch-finally+ TTL 三重兜底。 - 锁过期 → 看门狗动态续期,UUID + Lua 防误删,业务幂等兜底崩溃场景。
- 单节点故障 → RedLock 多节点半数加锁,提升锁服务高可用。
- 生产落地 → 直接用 Redisson,不重复造轮子。
最后再强调一句:没有银弹。Redis 分布式锁在极端场景下(GC STW、时钟漂移、看门狗崩溃)依然有失效的可能,对于正确性要求极高的业务,必须配合业务幂等、数据库乐观锁、状态机校验等多重防线,而不是把所有宝都压在分布式锁上。