黑马点评-分布式锁-03_lua_atomic_unlock

黑马点评分布式锁三:为什么判断了锁归属,还要用 Lua 解锁?

本文继续整理黑马点评 Redis 实战篇第 4 章「分布式锁」。

上一篇讲了 Redis 分布式锁的基础版本:用 setIfAbsent 抢锁,给锁设置过期时间,并在 value 中保存线程标识。

这一篇讲 4.44.8:为什么直接删除锁会误删别人?为什么"先判断是不是自己的锁,再删除"仍然不够?Lua 脚本到底解决了什么问题?


1. 这篇文章解决什么问题

在 Redis 分布式锁版本一中,解锁可能会写成:

java 复制代码
public void unlock() {
    stringRedisTemplate.delete(KEY_PREFIX + name);
}

这看起来很自然:

text 复制代码
我业务执行完了,我把锁删掉。

但这里藏着一个很危险的问题:

你删掉的锁,可能已经不是你自己的锁。

于是课程继续演进:

text 复制代码
第一步:直接 delete 解锁
    ↓
问题:可能误删别人的锁
    ↓
第二步:value 存线程标识,删除前先判断
    ↓
问题:GET 判断和 DEL 删除不是原子操作
    ↓
第三步:用 Lua 把判断和删除合成 Redis 端一次原子操作

本文就把这条演进链讲透。


2. 为什么会误删别人的锁

先看最朴素的解锁:

java 复制代码
public void unlock() {
    stringRedisTemplate.delete(KEY_PREFIX + name);
}

它的问题是:

text 复制代码
不管当前 Redis 里的锁是谁加的,我都直接删除。

如果业务一切正常,没问题。

但分布式锁一定要考虑异常时序。


3. 误删场景一:锁过期后被别人重新拿到

假设锁过期时间是 10 秒。

线程 A 先拿到锁:

text 复制代码
lock:order:10 -> A
TTL = 10s

然后线程 A 执行业务时卡住了。

可能原因很多:

text 复制代码
1. 业务代码执行慢。
2. 数据库调用卡住。
3. 网络抖动。
4. JVM 发生较长停顿。
5. 线程被调度挂起。

重点不是具体原因,而是:

text 复制代码
A 没死,但超过了锁的过期时间。

10 秒后,Redis 自动删除锁。

这时线程 B 也来处理同一个用户的下单请求。

B 发现:

text 复制代码
lock:order:10 不存在

于是 B 成功拿到同名新锁:

text 复制代码
lock:order:10 -> B

此时 A 恢复执行,终于走到 unlock()

如果 A 直接执行:

java 复制代码
delete("lock:order:10")

那它删掉的就是 B 的锁。


4. 为什么 B 会拿到同名新锁

这里有一个初学者常见疑惑:

B 为什么拿到的是同名新锁?

因为 A 和 B 操作的是同一个业务对象。

比如它们都是:

text 复制代码
用户 10 的下单请求

所以它们本来就应该竞争同一把业务锁:

text 复制代码
lock:order:10

锁过期后,这个 key 被 Redis 删除。

B 再来执行 SET key value NX EX timeout 时,发现 key 不存在,于是就能重新创建这个 key。

所以叫:

text 复制代码
同名的新锁

key 名字一样,但 value 已经从 A 的线程标识变成 B 的线程标识。


5. 误删流程图

线程B Redis 线程A 线程B Redis 线程A #mermaid-svg-QYzuTJuB7G5piImc{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-QYzuTJuB7G5piImc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QYzuTJuB7G5piImc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QYzuTJuB7G5piImc .error-icon{fill:#552222;}#mermaid-svg-QYzuTJuB7G5piImc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QYzuTJuB7G5piImc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QYzuTJuB7G5piImc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QYzuTJuB7G5piImc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QYzuTJuB7G5piImc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QYzuTJuB7G5piImc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QYzuTJuB7G5piImc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QYzuTJuB7G5piImc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QYzuTJuB7G5piImc .marker.cross{stroke:#333333;}#mermaid-svg-QYzuTJuB7G5piImc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QYzuTJuB7G5piImc p{margin:0;}#mermaid-svg-QYzuTJuB7G5piImc .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QYzuTJuB7G5piImc text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-QYzuTJuB7G5piImc .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QYzuTJuB7G5piImc .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-QYzuTJuB7G5piImc .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-QYzuTJuB7G5piImc .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-QYzuTJuB7G5piImc #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-QYzuTJuB7G5piImc .sequenceNumber{fill:white;}#mermaid-svg-QYzuTJuB7G5piImc #sequencenumber{fill:#333;}#mermaid-svg-QYzuTJuB7G5piImc #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-QYzuTJuB7G5piImc .messageText{fill:#333;stroke:none;}#mermaid-svg-QYzuTJuB7G5piImc .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QYzuTJuB7G5piImc .labelText,#mermaid-svg-QYzuTJuB7G5piImc .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-QYzuTJuB7G5piImc .loopText,#mermaid-svg-QYzuTJuB7G5piImc .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-QYzuTJuB7G5piImc .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-QYzuTJuB7G5piImc .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-QYzuTJuB7G5piImc .noteText,#mermaid-svg-QYzuTJuB7G5piImc .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-QYzuTJuB7G5piImc .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QYzuTJuB7G5piImc .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QYzuTJuB7G5piImc .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QYzuTJuB7G5piImc .actorPopupMenu{position:absolute;}#mermaid-svg-QYzuTJuB7G5piImc .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-QYzuTJuB7G5piImc .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QYzuTJuB7G5piImc .actor-man circle,#mermaid-svg-QYzuTJuB7G5piImc line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-QYzuTJuB7G5piImc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SET lock:order:10 = A NX EX 10加锁成功执行业务,发生阻塞10秒后锁自动过期SET lock:order:10 = B NX EX 10加锁成功恢复执行DEL lock:order:10删除了B的锁

这就是误删别人的锁。

本质是:

旧线程恢复执行时,锁的归属已经变了。


6. 第一层修复:删除前先判断锁归属

为了解决直接删除的问题,讲义提出:

text 复制代码
加锁时把线程标识存入 value。
解锁时先读取 value。
如果 value 等于当前线程标识,才删除。

加锁代码:

java 复制代码
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

@Override
public boolean tryLock(long timeoutSec) {
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}

解锁代码:

java 复制代码
public void unlock() {
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    if (threadId.equals(id)) {
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

这段代码的业务含义是:

text 复制代码
1. 我先查 Redis 里的锁 value。
2. 看看这个 value 是不是我的线程标识。
3. 如果是我的,说明锁还属于我,可以删。
4. 如果不是我的,说明锁已经属于别人,不能删。

这个思路是正确的。

它解决了"裸删锁"的大问题。


7. 为什么只用线程 id 不够

线程标识通常写成:

java 复制代码
ID_PREFIX + Thread.currentThread().getId()

其中:

java 复制代码
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

ID_PREFIX 可以理解成当前 JVM 实例的随机标识。

Thread.currentThread().getId() 是当前线程 id。

为什么不只用线程 id?

因为不同 JVM 中可能都有线程 17。

如果只存:

text 复制代码
17

跨 JVM 可能撞。

所以更稳的写法是:

text 复制代码
JVM随机前缀 + 线程id

比如:

text 复制代码
a8f3c91b-17

8. 新问题:判断了为什么还不够

到这里可能会产生新的疑惑:

我都已经判断锁是不是自己的了,为什么还要 Lua?

问题在于:

text 复制代码
GET 判断和 DEL 删除是两条 Redis 命令。

也就是说,这段 Java 代码:

java 复制代码
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (threadId.equals(id)) {
    stringRedisTemplate.delete(KEY_PREFIX + name);
}

在 Redis 层面不是一个整体。

它拆成了:

text 复制代码
1. GET lock:order:10
2. Java 判断 value 是否相等
3. DEL lock:order:10

中间可能插入其他事件。

这就是原子性问题。


9. 什么叫原子性

原子性可以简单理解为:

一组操作要么完整执行完,中间不能被别人插进来;要么就不执行。

在这里,我们希望:

text 复制代码
判断锁归属
删除锁

这两件事是一个不可拆分的整体。

但 Java 中先 GETDEL 不是整体。

中间可能发生:

text 复制代码
锁过期
其他线程重新加锁

10. 极端误删时序:判断正确,删除时已经不正确

来看一个更极端的场景。

线程 A 拿到了锁:

text 复制代码
lock:order:10 -> A

然后 A 准备解锁。

它先执行:

java 复制代码
String id = redis.get("lock:order:10");

此时 Redis 返回:

text 复制代码
A

A 判断:

text 复制代码
锁是我的,可以删。

但就在它执行 delete 之前,锁过期了。

线程 B 立刻拿到了新锁:

text 复制代码
lock:order:10 -> B

然后 A 继续执行:

java 复制代码
redis.delete("lock:order:10");

结果又把 B 的锁删了。

流程图:
线程B Redis 线程A 线程B Redis 线程A #mermaid-svg-eeRsuPHMTx7UPxxm{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-eeRsuPHMTx7UPxxm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eeRsuPHMTx7UPxxm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eeRsuPHMTx7UPxxm .error-icon{fill:#552222;}#mermaid-svg-eeRsuPHMTx7UPxxm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eeRsuPHMTx7UPxxm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eeRsuPHMTx7UPxxm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eeRsuPHMTx7UPxxm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eeRsuPHMTx7UPxxm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eeRsuPHMTx7UPxxm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eeRsuPHMTx7UPxxm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eeRsuPHMTx7UPxxm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eeRsuPHMTx7UPxxm .marker.cross{stroke:#333333;}#mermaid-svg-eeRsuPHMTx7UPxxm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eeRsuPHMTx7UPxxm p{margin:0;}#mermaid-svg-eeRsuPHMTx7UPxxm .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eeRsuPHMTx7UPxxm text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-eeRsuPHMTx7UPxxm .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-eeRsuPHMTx7UPxxm .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-eeRsuPHMTx7UPxxm .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-eeRsuPHMTx7UPxxm .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-eeRsuPHMTx7UPxxm #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-eeRsuPHMTx7UPxxm .sequenceNumber{fill:white;}#mermaid-svg-eeRsuPHMTx7UPxxm #sequencenumber{fill:#333;}#mermaid-svg-eeRsuPHMTx7UPxxm #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-eeRsuPHMTx7UPxxm .messageText{fill:#333;stroke:none;}#mermaid-svg-eeRsuPHMTx7UPxxm .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eeRsuPHMTx7UPxxm .labelText,#mermaid-svg-eeRsuPHMTx7UPxxm .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-eeRsuPHMTx7UPxxm .loopText,#mermaid-svg-eeRsuPHMTx7UPxxm .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-eeRsuPHMTx7UPxxm .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-eeRsuPHMTx7UPxxm .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-eeRsuPHMTx7UPxxm .noteText,#mermaid-svg-eeRsuPHMTx7UPxxm .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-eeRsuPHMTx7UPxxm .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eeRsuPHMTx7UPxxm .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eeRsuPHMTx7UPxxm .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eeRsuPHMTx7UPxxm .actorPopupMenu{position:absolute;}#mermaid-svg-eeRsuPHMTx7UPxxm .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-eeRsuPHMTx7UPxxm .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eeRsuPHMTx7UPxxm .actor-man circle,#mermaid-svg-eeRsuPHMTx7UPxxm line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-eeRsuPHMTx7UPxxm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET lock:order:10返回 A判断成功,锁属于自己锁过期,自动删除SET lock:order:10 = B NX EX 10B 加锁成功DEL lock:order:10删除了 B 的锁

这说明:

判断时正确,不代表删除时仍然正确。

根因就是:

text 复制代码
GET + 判断 + DEL 不是原子操作。

11. Lua 为什么能解决这个问题

既然问题出在多条 Redis 命令分开执行,那解决思路就是:

把"判断锁归属 + 删除锁"放到 Redis 服务器内部一次执行完。

Redis 提供了 Lua 脚本功能。

我们可以在 Lua 脚本里写多条 Redis 命令。

Redis 执行脚本时,会把脚本作为一个整体执行。

在脚本执行过程中,不会有其他 Redis 命令插进来。

所以 Lua 在这里解决的是:

text 复制代码
多条 Redis 操作的原子性问题。

它不是为了炫技。

它就是为了让:

text 复制代码
GET 判断 + DEL 删除

变成一个不可插队的整体。


12. unlock.lua 逐行解释

项目中的 Lua 脚本是:

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

这段脚本很短,但非常关键。


13. KEYS1 是什么

KEYS[1] 表示 Java 调用 Lua 时传入的第一个 key 参数。

在这里,它就是锁 key。

比如:

text 复制代码
lock:order:10

所以:

lua 复制代码
redis.call('GET', KEYS[1])

就相当于:

redis 复制代码
GET lock:order:10

14. ARGV1 是什么

ARGV[1] 表示 Java 调用 Lua 时传入的第一个普通参数。

在这里,它是当前线程标识。

比如:

text 复制代码
a8f3c91b-17

所以第一行:

lua 复制代码
if (redis.call('GET', KEYS[1]) == ARGV[1]) then

意思是:

text 复制代码
如果 Redis 中锁的 value 等于当前线程标识

也就是:

text 复制代码
如果这把锁确实属于我

15. DEL 做了什么

如果判断成立:

lua 复制代码
return redis.call('DEL', KEYS[1])

就删除锁 key。

如果判断不成立:

lua 复制代码
return 0

什么都不删。

所以整段 Lua 可以翻译成一句话:

如果锁是我的,就删;如果锁不是我的,就不动。

最关键的是:

这句判断和删除在 Redis 内部一次性执行完成。


16. Java 怎么加载 Lua 脚本

项目中用 DefaultRedisScript 表示 Lua 脚本:

java 复制代码
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

static {
    UNLOCK_SCRIPT = new DefaultRedisScript<>();
    UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    UNLOCK_SCRIPT.setResultType(Long.class);
}

逐行看。

DefaultRedisScript 是什么

DefaultRedisScript 是 Spring Data Redis 提供的脚本封装对象。

它用来告诉 RedisTemplate:

text 复制代码
我要执行一段 Redis Lua 脚本。

ClassPathResource 是什么

java 复制代码
new ClassPathResource("unlock.lua")

表示从类路径下加载 unlock.lua

在 Spring Boot 项目里,src/main/resources 下的文件会进入类路径。

所以这里能找到:

text 复制代码
unlock.lua

setResultType 是什么

java 复制代码
UNLOCK_SCRIPT.setResultType(Long.class);

表示这个 Lua 脚本返回值类型是 Long

因为:

lua 复制代码
return redis.call('DEL', KEYS[1])

或者:

lua 复制代码
return 0

最终都是数字。


17. Java 怎么执行 Lua 脚本

最终版 unlock() 是:

java 复制代码
@Override
public void unlock() {
    stringRedisTemplate.execute(
            UNLOCK_SCRIPT,
            Collections.singletonList(KEY_PREFIX + name),
            ID_PREFIX + Thread.currentThread().getId());
}

这个 execute 有三个关键部分。

第一个参数:UNLOCK_SCRIPT

java 复制代码
UNLOCK_SCRIPT

表示要执行哪个 Lua 脚本。

也就是刚才加载的 unlock.lua

第二个参数:keys

java 复制代码
Collections.singletonList(KEY_PREFIX + name)

这是传给 Lua 的 KEYS 数组。

这里只有一个 key。

比如:

text 复制代码
lock:order:10

在 Lua 中对应:

lua 复制代码
KEYS[1]

第三个参数:args

java 复制代码
ID_PREFIX + Thread.currentThread().getId()

这是传给 Lua 的 ARGV 数组。

这里只有一个参数,也就是当前线程标识。

在 Lua 中对应:

lua 复制代码
ARGV[1]

所以整段 Java 调用可以翻译成:

text 复制代码
请 Redis 执行 unlock.lua。
KEYS[1] = 当前锁 key。
ARGV[1] = 当前线程标识。

18. Lua 解锁完整流程图

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

业务执行完,调用 unlock
Java 计算锁 key
Java 计算当前线程标识
execute 执行 unlock.lua
Redis 内部 GET KEYS1
锁 value 是否等于 ARGV1?
DEL KEYS1
return 0,不删除

这张图的重点是:

text 复制代码
判断和删除都在 Redis 内部完成。

19. 手写 Redis 锁最终版本总结

把第四章手写 Redis 锁完整串起来:

text 复制代码
1. 本地锁只能锁当前 JVM,所以需要分布式锁。
2. Redis 是共享中间件,可以让多个服务实例竞争同一个锁 key。
3. 获取锁用 SET key value NX EX timeout。
4. NX 保证互斥。
5. EX 保证异常时锁能自动释放。
6. value 保存线程标识,便于判断锁归属。
7. 直接 delete 可能误删别人的锁。
8. 先 GET 判断再 DEL 仍然不是原子操作。
9. Lua 把判断和删除合成 Redis 端原子操作。

20. 但手写锁还有一个问题

第四章末尾会提到:

text 复制代码
如果业务执行时间超过锁过期时间,锁还是会提前释放。

Lua 可以解决:

text 复制代码
不要误删别人的锁。

但它不能解决:

text 复制代码
业务没执行完,锁已经过期。

也就是说,Lua 只是保证解锁安全。

它不能自动给锁续期。

这个问题后面会引出 Redisson 的看门狗机制。

不过这是下一章内容。

本文只关注第四章的手写 Redis 分布式锁。


21. 本篇易错点

1. 加过期时间是为了避免死锁,但也会引出锁提前释放问题

没有过期时间,服务挂了可能死锁。

有过期时间,业务超时可能导致锁被别人重新拿到。

2. 线程 A 不是从线程 B 手里抢回运行权

线程 A 可能只是之前阻塞了,后来恢复继续执行。

它恢复时,锁可能已经过期并被 B 重新持有。

3. 判断锁归属只能解决一部分误删问题

如果 GETDEL 分开执行,判断之后锁状态仍可能变化。

4. Lua 解决的是原子性

Lua 的重点不是语法,而是:

text 复制代码
让多条 Redis 操作一次性执行。

5. KEYS 和 ARGV 不要混

KEYS[1] 是锁 key。

ARGV[1] 是当前线程标识。


22. 面试怎么回答

如果面试官问:Redis 分布式锁为什么不能直接 delete 解锁?

可以这样回答:

因为持锁线程的业务执行时间可能超过锁过期时间,锁过期后可能已经被其他线程重新获取。如果旧线程恢复后直接删除同名 key,就可能把别人的新锁删掉,所以不能直接 delete。

如果面试官问:为什么要在锁 value 中保存线程标识?

可以这样回答:

保存线程标识是为了释放锁时判断锁归属。解锁前先比较 Redis 中的 value 是否等于当前线程标识,只有锁属于自己时才允许删除,避免误删其他线程持有的锁。

如果面试官问:既然判断了锁归属,为什么还要 Lua?

可以这样回答:

因为如果在 Java 中先 GET 判断,再 DEL 删除,这是两条 Redis 命令,中间可能发生锁过期并被其他线程重新获取。这样判断时锁属于自己,但删除时锁已经属于别人。Lua 可以把判断和删除放到 Redis 内部一次执行,保证原子性。

如果面试官问:Lua 脚本里的 KEYS[1]ARGV[1] 分别是什么?

可以这样回答:

KEYS[1] 是 Java 调用脚本时传入的锁 key,例如 lock:order:10ARGV[1] 是当前线程的唯一标识。脚本会判断 GET KEYS[1] 是否等于 ARGV[1],相等才删除锁。


23. 总结

第四章手写 Redis 分布式锁的演进很清晰:

text 复制代码
本地锁只能锁当前 JVM
    ↓
用 Redis key 实现跨 JVM 共享锁
    ↓
setIfAbsent 保证互斥
    ↓
过期时间避免死锁
    ↓
线程标识避免裸删别人的锁
    ↓
Lua 保证判断和删除的原子性

最重要的是不要只背代码,而要理解每一步解决的问题:

text 复制代码
setIfAbsent:解决谁先拿到锁的问题。
过期时间:解决持锁线程异常导致死锁的问题。
线程标识:解决锁归属判断的问题。
Lua:解决判断和删除不是原子操作的问题。

到这里,手写 Redis 分布式锁已经形成了一个比较完整的版本。

但它仍然不是工业级最终答案。

因为如果业务执行时间超过锁的 TTL,锁还是会提前释放。

这个问题会在后续 Redisson 中继续解决。

相关推荐
多工坊1 小时前
The content of elements must consist of well-formed character data or markup.
java
linmoo19861 小时前
Java踩坑系列之二:ThreadLocal内存泄漏
java·内存泄漏·threadlocal·踩坑
码不停蹄的玄黓1 小时前
MySQL唯一索引能否做主键索引
数据库·sql·mysql
27669582921 小时前
拼多多m端/小程序 encrypt_info
java·小程序·apache·encrypt_info·encrypt_info解密·拼多多小程序·拼多多m端
码不停蹄的玄黓1 小时前
Java 应用 CPU 过高排查全流程
java·开发语言·python
许彰午1 小时前
11_Java集合框架概述
java·windows·python
小谢小哥1 小时前
64-依赖冲突解决详解
java·后端·架构