黑马点评分布式锁三:为什么判断了锁归属,还要用 Lua 解锁?
本文继续整理黑马点评 Redis 实战篇第 4 章「分布式锁」。
上一篇讲了 Redis 分布式锁的基础版本:用
setIfAbsent抢锁,给锁设置过期时间,并在 value 中保存线程标识。这一篇讲
4.4到4.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 中先 GET 再 DEL 不是整体。
中间可能发生:
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. 判断锁归属只能解决一部分误删问题
如果 GET 和 DEL 分开执行,判断之后锁状态仍可能变化。
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:10;ARGV[1]是当前线程的唯一标识。脚本会判断GET KEYS[1]是否等于ARGV[1],相等才删除锁。
23. 总结
第四章手写 Redis 分布式锁的演进很清晰:
text
本地锁只能锁当前 JVM
↓
用 Redis key 实现跨 JVM 共享锁
↓
setIfAbsent 保证互斥
↓
过期时间避免死锁
↓
线程标识避免裸删别人的锁
↓
Lua 保证判断和删除的原子性
最重要的是不要只背代码,而要理解每一步解决的问题:
text
setIfAbsent:解决谁先拿到锁的问题。
过期时间:解决持锁线程异常导致死锁的问题。
线程标识:解决锁归属判断的问题。
Lua:解决判断和删除不是原子操作的问题。
到这里,手写 Redis 分布式锁已经形成了一个比较完整的版本。
但它仍然不是工业级最终答案。
因为如果业务执行时间超过锁的 TTL,锁还是会提前释放。
这个问题会在后续 Redisson 中继续解决。