黑马点评-分布式锁-02_simple_redis_lock_setnx

黑马点评分布式锁二: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;

可能触发自动拆箱。

如果 successnull,就会出现空指针异常。

所以项目写:

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?
相关推荐
原创小甜甜1 小时前
OOM 排查复盘:Hutool 序列化 Request 导致 Java Heap Space
java·开发语言·python
列星随旋1 小时前
矩阵快速幂
java·算法·矩阵
数据库小学妹1 小时前
数据库高可用架构实战:从主从复制到两地三中心的四层演进与避坑
数据库·经验分享·架构·dba
Gong-Yu1 小时前
MySQL数据库运维(1)
运维·数据库·mysql·慢查询
柏舟飞流1 小时前
StarRocks: 新一代极速全场景MPP数据库
数据库
萨小耶1 小时前
[Java学习日记10】聊聊checked exception和runtime exception
java·开发语言·学习
超梦dasgg1 小时前
IDEA(IntelliJ IDEA)超详细基础使用教程
java·ide·intellij-idea
404号扳手1 小时前
Java 进阶知识(八)
java·后端
Stick_ZYZ1 小时前
从项目启动到 Milvus 向量检索,我把 RAG 项目链路又打通了一层
java·人工智能·经验分享·ai·milvus