黑马点评分布式锁二:Redis 锁为什么要用 setIfAbsent、过期时间和线程标识?
本文继续整理黑马点评 Redis 实战篇第 4 章「分布式锁」。
上一篇讲清楚了为什么
synchronized只能解决单 JVM 内的并发问题,到了多实例部署时就需要分布式锁。这一篇进入
4.2 Redis 分布式锁的实现核心思路和4.3 实现分布式锁版本一:Redis 锁到底怎么写?SimpleRedisLock是谁调用的?setIfAbsent、过期时间、线程标识分别解决什么问题?
1. 这篇文章解决什么问题
学 Redis 分布式锁时,很容易只记住一句:
text
用 SETNX 加锁。
但真正看代码时,会冒出一串问题:
text
SimpleRedisLock 是谁 new 出来的?
构造方法里的 name 是谁传的?
name 是用户 id 吗?
tryLock 里的 key 到底长什么样?
setIfAbsent 到底相当于什么 Redis 命令?
为什么加锁要带过期时间?
为什么 value 里还要保存线程标识?
为什么不能直接写 lock:order:10 -> 1?
本文就围绕这些问题讲清楚。
先给结论:
Redis 分布式锁的版本一,本质是用一个 Redis key 表示一把业务锁,谁先通过
setIfAbsent成功创建这个 key,谁就拿到锁;加锁时要设置过期时间防止死锁,value 里保存线程标识是为了后续安全解锁。
2. 先把业务调用链放回来
工具类不能脱离业务讲。
如果只看 SimpleRedisLock,很容易不知道:
text
谁调用它?
为什么传这个 name?
tryLock 成功后执行什么?
unlock 在哪里调用?
讲义中的业务代码大致是这样:
java
Long userId = UserHolder.getUser().getId();
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
boolean isLock = lock.tryLock(1200);
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
这段代码的业务含义是:
text
1. 当前用户发起秒杀请求。
2. 根据 userId 创建一把下单锁。
3. 先尝试抢锁。
4. 抢不到,说明同用户请求正在处理,直接失败。
5. 抢到锁,才进入真正创建订单逻辑。
6. 不管业务成功还是失败,finally 中释放锁。
流程图如下:
#mermaid-svg-ZmSzWeuFSWHLp7tS{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-ZmSzWeuFSWHLp7tS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZmSzWeuFSWHLp7tS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZmSzWeuFSWHLp7tS .error-icon{fill:#552222;}#mermaid-svg-ZmSzWeuFSWHLp7tS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZmSzWeuFSWHLp7tS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZmSzWeuFSWHLp7tS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZmSzWeuFSWHLp7tS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZmSzWeuFSWHLp7tS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZmSzWeuFSWHLp7tS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZmSzWeuFSWHLp7tS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZmSzWeuFSWHLp7tS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZmSzWeuFSWHLp7tS .marker.cross{stroke:#333333;}#mermaid-svg-ZmSzWeuFSWHLp7tS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZmSzWeuFSWHLp7tS p{margin:0;}#mermaid-svg-ZmSzWeuFSWHLp7tS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZmSzWeuFSWHLp7tS .cluster-label text{fill:#333;}#mermaid-svg-ZmSzWeuFSWHLp7tS .cluster-label span{color:#333;}#mermaid-svg-ZmSzWeuFSWHLp7tS .cluster-label span p{background-color:transparent;}#mermaid-svg-ZmSzWeuFSWHLp7tS .label text,#mermaid-svg-ZmSzWeuFSWHLp7tS span{fill:#333;color:#333;}#mermaid-svg-ZmSzWeuFSWHLp7tS .node rect,#mermaid-svg-ZmSzWeuFSWHLp7tS .node circle,#mermaid-svg-ZmSzWeuFSWHLp7tS .node ellipse,#mermaid-svg-ZmSzWeuFSWHLp7tS .node polygon,#mermaid-svg-ZmSzWeuFSWHLp7tS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZmSzWeuFSWHLp7tS .rough-node .label text,#mermaid-svg-ZmSzWeuFSWHLp7tS .node .label text,#mermaid-svg-ZmSzWeuFSWHLp7tS .image-shape .label,#mermaid-svg-ZmSzWeuFSWHLp7tS .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZmSzWeuFSWHLp7tS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZmSzWeuFSWHLp7tS .rough-node .label,#mermaid-svg-ZmSzWeuFSWHLp7tS .node .label,#mermaid-svg-ZmSzWeuFSWHLp7tS .image-shape .label,#mermaid-svg-ZmSzWeuFSWHLp7tS .icon-shape .label{text-align:center;}#mermaid-svg-ZmSzWeuFSWHLp7tS .node.clickable{cursor:pointer;}#mermaid-svg-ZmSzWeuFSWHLp7tS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZmSzWeuFSWHLp7tS .arrowheadPath{fill:#333333;}#mermaid-svg-ZmSzWeuFSWHLp7tS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZmSzWeuFSWHLp7tS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZmSzWeuFSWHLp7tS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZmSzWeuFSWHLp7tS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZmSzWeuFSWHLp7tS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZmSzWeuFSWHLp7tS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZmSzWeuFSWHLp7tS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZmSzWeuFSWHLp7tS .cluster text{fill:#333;}#mermaid-svg-ZmSzWeuFSWHLp7tS .cluster span{color:#333;}#mermaid-svg-ZmSzWeuFSWHLp7tS 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-ZmSzWeuFSWHLp7tS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZmSzWeuFSWHLp7tS rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZmSzWeuFSWHLp7tS .icon-shape,#mermaid-svg-ZmSzWeuFSWHLp7tS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZmSzWeuFSWHLp7tS .icon-shape p,#mermaid-svg-ZmSzWeuFSWHLp7tS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZmSzWeuFSWHLp7tS .icon-shape .label rect,#mermaid-svg-ZmSzWeuFSWHLp7tS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZmSzWeuFSWHLp7tS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZmSzWeuFSWHLp7tS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZmSzWeuFSWHLp7tS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
用户发起秒杀请求
获取当前 userId
new SimpleRedisLock('order:' + userId)
tryLock(timeoutSec)
是否加锁成功?
返回不允许重复下单
调用 createVoucherOrder
查询是否已下单
扣减库存
创建订单
finally unlock
所以 SimpleRedisLock 不是孤立工具类。
它是秒杀下单业务为了防止同一用户重复下单而使用的锁实现。
3. ILock 接口为什么只有两个方法
项目中有一个锁接口:
java
public interface ILock {
boolean tryLock(long timeoutSec);
void unlock();
}
它只有两个方法。
这很符合锁的本质:
text
1. 获取锁
2. 释放锁
tryLock 是什么
tryLock 表示尝试获取锁。
输入:
text
timeoutSec:锁的过期时间,单位是秒
输出:
text
true:获取锁成功
false:获取锁失败
它不是一直阻塞等待。
讲义这里强调的是:
text
尝试一次,成功返回 true,失败返回 false。
unlock 是什么
unlock 表示释放锁。
业务执行完后要调用它。
如果不释放锁,其他请求就可能一直拿不到锁。
4. SimpleRedisLock 是什么
SimpleRedisLock 实现了 ILock:
java
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
}
它可以理解成:
用 Redis 手写出来的一把简易分布式锁。
它的构造方法是:
java
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
这里有两个参数。
name
name 是业务锁名称。
在秒杀下单中传入:
java
"order:" + userId
如果 userId = 10,那么:
text
name = order:10
它不是单纯的用户 id,而是:
text
业务类型 + 用户 id
这样语义更清楚:
text
order:10 表示用户 10 的下单锁
stringRedisTemplate
这是 Spring Data Redis 提供的 Redis 操作对象。
SimpleRedisLock 需要用它去 Redis 里创建锁 key、删除锁 key。
5. Redis 锁的 key 到底长什么样
SimpleRedisLock 中定义了前缀:
java
private static final String KEY_PREFIX = "lock:";
加锁时会拼出 key:
java
String key = KEY_PREFIX + name;
如果业务代码传入:
java
new SimpleRedisLock("order:" + userId, stringRedisTemplate)
并且:
text
userId = 10
那么最终 Redis key 是:
text
lock:order:10
这个 key 的含义是:
text
用户 10 的下单锁
所以你可以这样理解:
锁的 key 表达"这把锁保护的是哪个业务对象"。
在这里保护的是:
text
同一个用户的下单行为
6. 为什么同一个用户要竞争同名锁
假设用户 10 连续点击两次秒杀按钮。
两个请求分别由线程 A 和线程 B 处理。
它们都会执行:
java
new SimpleRedisLock("order:" + userId, stringRedisTemplate)
因为 userId 都是 10,所以两边的锁 key 都是:
text
lock:order:10
这就是同名锁。
同名不是 bug,而是故意这样设计。
因为我们希望:
text
同一个用户的两个请求竞争同一把锁。
如果两个请求生成两个不同 key,那它们就不会互斥,一人一单又会失效。
7. tryLock 的核心代码
tryLock 的代码大致如下:
java
@Override
public boolean tryLock(long timeoutSec) {
String key = KEY_PREFIX + name;
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
这段代码做了四件事:
text
1. 拼锁 key。
2. 生成当前线程标识。
3. 使用 setIfAbsent 尝试写入 Redis。
4. 返回是否加锁成功。
下面一行一行拆。
8. setIfAbsent 是什么
核心代码是:
java
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, threadId, timeoutSec, TimeUnit.SECONDS);
setIfAbsent 字面意思是:
text
如果不存在,才设置。
也就是:
text
key 不存在:设置成功,返回 true
key 已存在:设置失败,返回 false
它对应 Redis 思想就是:
redis
SET lock:order:10 threadId NX EX 1200
其中:
text
NX:key 不存在才设置
EX:设置过期时间,单位是秒
以前常说的 SETNX 也是类似思想:
redis
SETNX lock:order:10 threadId
但更推荐把设置值和过期时间合成一条命令:
redis
SET lock:order:10 threadId NX EX 1200
这样可以保证:
text
加锁和设置过期时间一起完成。
9. setIfAbsent 为什么适合做锁
锁最核心的要求是:
text
同一时刻只能有一个线程拿到锁。
Redis 的 setIfAbsent 刚好满足这个要求。
假设两个线程同时抢:
text
线程A:SET lock:order:10 A NX EX 1200
线程B:SET lock:order:10 B NX EX 1200
Redis 对同一个 key 的命令执行是串行的。
所以只会有一个线程成功。
比如线程 A 先成功:
text
Redis 中出现 lock:order:10 -> A
线程 B 再执行时发现 key 已存在,就失败。
流程图:
Redis 线程B 线程A Redis 线程B 线程A #mermaid-svg-CKME8n0tPlVtb588{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-CKME8n0tPlVtb588 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CKME8n0tPlVtb588 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CKME8n0tPlVtb588 .error-icon{fill:#552222;}#mermaid-svg-CKME8n0tPlVtb588 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CKME8n0tPlVtb588 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CKME8n0tPlVtb588 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CKME8n0tPlVtb588 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CKME8n0tPlVtb588 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CKME8n0tPlVtb588 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CKME8n0tPlVtb588 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CKME8n0tPlVtb588 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CKME8n0tPlVtb588 .marker.cross{stroke:#333333;}#mermaid-svg-CKME8n0tPlVtb588 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CKME8n0tPlVtb588 p{margin:0;}#mermaid-svg-CKME8n0tPlVtb588 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CKME8n0tPlVtb588 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-CKME8n0tPlVtb588 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-CKME8n0tPlVtb588 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-CKME8n0tPlVtb588 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-CKME8n0tPlVtb588 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-CKME8n0tPlVtb588 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-CKME8n0tPlVtb588 .sequenceNumber{fill:white;}#mermaid-svg-CKME8n0tPlVtb588 #sequencenumber{fill:#333;}#mermaid-svg-CKME8n0tPlVtb588 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-CKME8n0tPlVtb588 .messageText{fill:#333;stroke:none;}#mermaid-svg-CKME8n0tPlVtb588 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CKME8n0tPlVtb588 .labelText,#mermaid-svg-CKME8n0tPlVtb588 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-CKME8n0tPlVtb588 .loopText,#mermaid-svg-CKME8n0tPlVtb588 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-CKME8n0tPlVtb588 .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-CKME8n0tPlVtb588 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-CKME8n0tPlVtb588 .noteText,#mermaid-svg-CKME8n0tPlVtb588 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-CKME8n0tPlVtb588 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CKME8n0tPlVtb588 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CKME8n0tPlVtb588 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CKME8n0tPlVtb588 .actorPopupMenu{position:absolute;}#mermaid-svg-CKME8n0tPlVtb588 .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-CKME8n0tPlVtb588 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CKME8n0tPlVtb588 .actor-man circle,#mermaid-svg-CKME8n0tPlVtb588 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-CKME8n0tPlVtb588 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SET lock:order:10 A NX EX 1200 true SET lock:order:10 B NX EX 1200 false
所以:
谁先成功创建锁 key,谁就拿到锁。
10. 为什么锁必须设置过期时间
如果只写:
redis
SETNX lock:order:10 A
但不设置过期时间,会有什么问题?
假设线程 A 拿到锁后,服务突然宕机,或者代码异常退出,没有执行 unlock()。
那么 Redis 中会一直存在:
text
lock:order:10 -> A
后续所有请求都会因为 key 已存在而加锁失败。
这就是死锁。
所以加锁时必须设置过期时间:
java
setIfAbsent(key, threadId, timeoutSec, TimeUnit.SECONDS)
它的意义是:
即使持锁线程异常退出,Redis 也会在超时后自动释放锁。
11. 为什么加锁和设置过期时间要放在一条命令里
有一种错误写法是:
java
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId);
stringRedisTemplate.expire(key, timeoutSec, TimeUnit.SECONDS);
看起来也能设置过期时间。
但这里有一个危险窗口:
text
setIfAbsent 成功
↓
服务还没来得及 expire
↓
服务宕机
↓
锁永不过期
所以必须用带过期时间的原子写法:
java
setIfAbsent(key, threadId, timeoutSec, TimeUnit.SECONDS)
这样加锁和设置 TTL 是一次 Redis 操作。
12. 为什么 value 不能随便写 1
一开始可能会觉得:
text
锁 key 存在就说明有人拿锁。
value 写什么都无所谓。
比如:
text
lock:order:10 -> 1
但后面解锁时会出问题。
假设线程 A 拿到了锁:
text
lock:order:10 -> 1
过一会儿锁过期了,线程 B 又拿到同一把业务锁:
text
lock:order:10 -> 1
此时线程 A 恢复执行,它怎么知道这把锁已经不是自己的?
不知道。
因为 value 都是 1。
所以 value 不能随便写。
它应该保存:
text
当前持锁者的唯一标识
13. 线程标识是怎么生成的
项目中有一行:
java
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
它会在类加载时生成一个随机前缀。
可以把它理解成:
text
当前 JVM 实例的唯一标识
然后加锁时再拼当前线程 id:
java
String threadId = ID_PREFIX + Thread.currentThread().getId();
比如:
text
ID_PREFIX = a8f3c91b-
Thread.currentThread().getId() = 17
最终:
text
threadId = a8f3c91b-17
这个值表示:
text
某个 JVM 实例中的某个线程
14. 为什么不能只用线程 id
因为线程 id 只在当前 JVM 内有意义。
比如:
text
JVM A 里可以有线程 17
JVM B 里也可以有线程 17
如果只存:
text
17
跨 JVM 时就可能撞。
所以要用:
text
JVM 随机前缀 + 线程 id
这样更能表示:
text
到底是哪一个服务实例里的哪一个线程持有锁。
15. return Boolean.TRUE.equals(success) 是为什么
setIfAbsent 返回的是 Boolean 包装类型,不是基本类型 boolean。
理论上它可能是:
text
true
false
null
如果直接写:
java
return success;
可能触发自动拆箱。
如果 success 是 null,就会出现空指针异常。
所以项目写:
java
return Boolean.TRUE.equals(success);
含义是:
text
只有 success 明确等于 true,才返回 true。
其它情况都返回 false。
这是一个小细节,但很实用。
16. 版本一的 unlock
讲义中最开始的解锁版本很朴素:
java
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
它的意思是:
text
业务执行完后,直接删除锁 key。
如果一切顺利,这当然能释放锁。
但这个版本有明显隐患:
text
它不判断这把锁是不是自己加的。
这会引出下一篇重点问题:
text
误删别人的锁。
17. 当前版本的整体流程
把 Redis 锁版本一串起来,流程是:
#mermaid-svg-qSbPIuCe4NfYTos8{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-qSbPIuCe4NfYTos8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qSbPIuCe4NfYTos8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qSbPIuCe4NfYTos8 .error-icon{fill:#552222;}#mermaid-svg-qSbPIuCe4NfYTos8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qSbPIuCe4NfYTos8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qSbPIuCe4NfYTos8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qSbPIuCe4NfYTos8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qSbPIuCe4NfYTos8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qSbPIuCe4NfYTos8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qSbPIuCe4NfYTos8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qSbPIuCe4NfYTos8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qSbPIuCe4NfYTos8 .marker.cross{stroke:#333333;}#mermaid-svg-qSbPIuCe4NfYTos8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qSbPIuCe4NfYTos8 p{margin:0;}#mermaid-svg-qSbPIuCe4NfYTos8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qSbPIuCe4NfYTos8 .cluster-label text{fill:#333;}#mermaid-svg-qSbPIuCe4NfYTos8 .cluster-label span{color:#333;}#mermaid-svg-qSbPIuCe4NfYTos8 .cluster-label span p{background-color:transparent;}#mermaid-svg-qSbPIuCe4NfYTos8 .label text,#mermaid-svg-qSbPIuCe4NfYTos8 span{fill:#333;color:#333;}#mermaid-svg-qSbPIuCe4NfYTos8 .node rect,#mermaid-svg-qSbPIuCe4NfYTos8 .node circle,#mermaid-svg-qSbPIuCe4NfYTos8 .node ellipse,#mermaid-svg-qSbPIuCe4NfYTos8 .node polygon,#mermaid-svg-qSbPIuCe4NfYTos8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qSbPIuCe4NfYTos8 .rough-node .label text,#mermaid-svg-qSbPIuCe4NfYTos8 .node .label text,#mermaid-svg-qSbPIuCe4NfYTos8 .image-shape .label,#mermaid-svg-qSbPIuCe4NfYTos8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-qSbPIuCe4NfYTos8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qSbPIuCe4NfYTos8 .rough-node .label,#mermaid-svg-qSbPIuCe4NfYTos8 .node .label,#mermaid-svg-qSbPIuCe4NfYTos8 .image-shape .label,#mermaid-svg-qSbPIuCe4NfYTos8 .icon-shape .label{text-align:center;}#mermaid-svg-qSbPIuCe4NfYTos8 .node.clickable{cursor:pointer;}#mermaid-svg-qSbPIuCe4NfYTos8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qSbPIuCe4NfYTos8 .arrowheadPath{fill:#333333;}#mermaid-svg-qSbPIuCe4NfYTos8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qSbPIuCe4NfYTos8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qSbPIuCe4NfYTos8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qSbPIuCe4NfYTos8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qSbPIuCe4NfYTos8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qSbPIuCe4NfYTos8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qSbPIuCe4NfYTos8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qSbPIuCe4NfYTos8 .cluster text{fill:#333;}#mermaid-svg-qSbPIuCe4NfYTos8 .cluster span{color:#333;}#mermaid-svg-qSbPIuCe4NfYTos8 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-qSbPIuCe4NfYTos8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qSbPIuCe4NfYTos8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-qSbPIuCe4NfYTos8 .icon-shape,#mermaid-svg-qSbPIuCe4NfYTos8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qSbPIuCe4NfYTos8 .icon-shape p,#mermaid-svg-qSbPIuCe4NfYTos8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qSbPIuCe4NfYTos8 .icon-shape .label rect,#mermaid-svg-qSbPIuCe4NfYTos8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qSbPIuCe4NfYTos8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qSbPIuCe4NfYTos8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qSbPIuCe4NfYTos8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
秒杀请求进入
获取 userId
创建 SimpleRedisLock: order:userId
拼 Redis key: lock:order:userId
生成 threadId
SET key threadId NX EX timeout
加锁成功?
返回不允许重复下单
执行业务: createVoucherOrder
finally unlock
删除锁 key
这套流程已经能解决:
text
多 JVM 下同一个用户并发请求竞争同一把 Redis 锁。
但还没有彻底解决:
text
安全解锁问题。
18. 本篇易错点
1. name 不是单纯 userId
name 是业务锁名称。
在下单场景中设计为:
text
order:userId
最终 Redis key 是:
text
lock:order:userId
2. 锁 key 表达"锁谁"
lock:order:10 表示:
text
用户 10 的下单锁。
同一个用户的请求应该竞争同一个 key。
3. 锁 value 表达"谁持有"
value 保存线程标识。
它不是随便写的。
后续解锁时要靠它判断:
text
这把锁是不是我加的。
4. 过期时间不是可选项
没有过期时间,一旦持锁线程异常退出,就可能死锁。
5. setIfAbsent 要带过期时间一起执行
加锁和设置过期时间分成两步,会产生中间宕机风险。
19. 面试怎么回答
如果面试官问:Redis 分布式锁的基本实现思路是什么?
可以这样回答:
可以用一个 Redis key 表示一把锁,多个服务实例都去竞争这个 key。获取锁时使用
SET key value NX EX timeout,只有 key 不存在时才能设置成功,设置成功说明拿到锁,失败说明锁已经被别人持有。value 通常保存当前线程或实例的唯一标识,过期时间用于避免服务异常导致死锁。
如果面试官问:为什么加锁要设置过期时间?
可以这样回答:
如果持锁线程在业务执行过程中异常退出,或者服务宕机,没有执行解锁逻辑,那么锁 key 会一直留在 Redis 中,后续请求永远拿不到锁。设置过期时间后,即使持锁线程异常,Redis 也能自动删除锁,避免死锁。
如果面试官问:为什么锁 value 要保存线程标识?
可以这样回答:
因为释放锁时需要判断这把锁是不是自己加的。如果 value 只是固定的
1,线程就无法区分当前锁属于谁,可能在自己的锁过期后误删别人的新锁。保存线程标识可以为后续安全解锁提供依据。
20. 总结
本篇讲的是 Redis 分布式锁最基础的一版。
核心是这句话:
用 Redis key 表示锁,用
setIfAbsent抢锁,用过期时间避免死锁,用线程标识记录锁的持有者。
这一版已经比本地锁更进一步。
因为锁状态放到了 Redis 中,多个 JVM 都能看到同一把锁。
但它还没有完全安全。
如果解锁时只是:
java
delete(key)
就可能误删别人的锁。
下一篇继续讲:
text
为什么会误删?
为什么"先判断再删除"还不够?
为什么最后要用 Lua?