欢迎光临
我们一直在努力

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

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

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

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

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


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

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

public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}

这看起来很自然:

我业务执行完了,我把锁删掉。

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

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

于是课程继续演进:

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

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


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

先看最朴素的解锁:

public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}

它的问题是:

不管当前 Redis 里的锁是谁加的,我都直接删除。

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

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


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

假设锁过期时间是 10 秒。

线程 A 先拿到锁:

lock:order:10 -> A
TTL = 10s

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

可能原因很多:

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

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

A 没死,但超过了锁的过期时间。

10 秒后,Redis 自动删除锁。

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

B 发现:

lock:order:10 不存在

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

lock:order:10 -> B

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

如果 A 直接执行:

delete("lock:order:10")

那它删掉的就是 B 的锁。


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

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

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

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

比如它们都是:

用户 10 的下单请求

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

lock:order:10

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

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

所以叫:

同名的新锁

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


5. 误删流程图

线程BRedis线程A线程BRedis线程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. 第一层修复:删除前先判断锁归属

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

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

加锁代码:

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);
}

解锁代码:

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);
}
}

这段代码的业务含义是:

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

这个思路是正确的。

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


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

线程标识通常写成:

ID_PREFIX + Thread.currentThread().getId()

其中:

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

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

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

为什么不只用线程 id?

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

如果只存:

17

跨 JVM 可能撞。

所以更稳的写法是:

JVM随机前缀 + 线程id

比如:

a8f3c91b-17


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

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

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

问题在于:

GET 判断和 DEL 删除是两条 Redis 命令。

也就是说,这段 Java 代码:

String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (threadId.equals(id)) {
stringRedisTemplate.delete(KEY_PREFIX + name);
}

在 Redis 层面不是一个整体。

它拆成了:

1. GET lock:order:10
2. Java 判断 value 是否相等
3. DEL lock:order:10

中间可能插入其他事件。

这就是原子性问题。


9. 什么叫原子性

原子性可以简单理解为:

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

在这里,我们希望:

判断锁归属
删除锁

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

但 Java 中先 GET 再 DEL 不是整体。

中间可能发生:

锁过期
其他线程重新加锁


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

来看一个更极端的场景。

线程 A 拿到了锁:

lock:order:10 -> A

然后 A 准备解锁。

它先执行:

String id = redis.get("lock:order:10");

此时 Redis 返回:

A

A 判断:

锁是我的,可以删。

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

线程 B 立刻拿到了新锁:

lock:order:10 -> B

然后 A 继续执行:

redis.delete("lock:order:10");

结果又把 B 的锁删了。

流程图:

线程BRedis线程A线程BRedis线程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 的锁

这说明:

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

根因就是:

GET + 判断 + DEL 不是原子操作。


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

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

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

Redis 提供了 Lua 脚本功能。

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

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

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

所以 Lua 在这里解决的是:

多条 Redis 操作的原子性问题。

它不是为了炫技。

它就是为了让:

GET 判断 + DEL 删除

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


12. unlock.lua 逐行解释

项目中的 Lua 脚本是:

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

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


13. KEYS[1] 是什么

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

在这里,它就是锁 key。

比如:

lock:order:10

所以:

redis.call('GET', KEYS[1])

就相当于:

GET lock:order:10


14. ARGV[1] 是什么

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

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

比如:

a8f3c91b-17

所以第一行:

if (redis.call('GET', KEYS[1]) == ARGV[1]) then

意思是:

如果 Redis 中锁的 value 等于当前线程标识

也就是:

如果这把锁确实属于我


15. DEL 做了什么

如果判断成立:

return redis.call('DEL', KEYS[1])

就删除锁 key。

如果判断不成立:

return 0

什么都不删。

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

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

最关键的是:

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


16. Java 怎么加载 Lua 脚本

项目中用 DefaultRedisScript 表示 Lua 脚本:

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:

我要执行一段 Redis Lua 脚本。

ClassPathResource 是什么

new ClassPathResource("unlock.lua")

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

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

所以这里能找到:

unlock.lua

setResultType 是什么

UNLOCK_SCRIPT.setResultType(Long.class);

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

因为:

return redis.call('DEL', KEYS[1])

或者:

return 0

最终都是数字。


17. Java 怎么执行 Lua 脚本

最终版 unlock() 是:

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

这个 execute 有三个关键部分。

第一个参数:UNLOCK_SCRIPT

UNLOCK_SCRIPT

表示要执行哪个 Lua 脚本。

也就是刚才加载的 unlock.lua。

第二个参数:keys

Collections.singletonList(KEY_PREFIX + name)

这是传给 Lua 的 KEYS 数组。

这里只有一个 key。

比如:

lock:order:10

在 Lua 中对应:

KEYS[1]

第三个参数:args

ID_PREFIX + Thread.currentThread().getId()

这是传给 Lua 的 ARGV 数组。

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

在 Lua 中对应:

ARGV[1]

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

请 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 KEYS[1]

锁 value 是否等于 ARGV[1]?

DEL KEYS[1]

return 0,不删除

这张图的重点是:

判断和删除都在 Redis 内部完成。


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

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

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. 但手写锁还有一个问题

第四章末尾会提到:

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

Lua 可以解决:

不要误删别人的锁。

但它不能解决:

业务没执行完,锁已经过期。

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

它不能自动给锁续期。

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

不过这是下一章内容。

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


21. 本篇易错点

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

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

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

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

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

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

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

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

4. Lua 解决的是原子性

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

让多条 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 分布式锁的演进很清晰:

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

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

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

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

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

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

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

赞(0)
未经允许不得转载:171主机测评 » 黑马点评-分布式锁-03_lua_atomic_unlock
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址