redis分布式锁与lua脚本的作用
分布式锁实现的核心安全机制。
我们来逐个深入解析:
✅ 问题一:“这里不是只有持有锁的进程才能释放锁么?”
答:理论上是这样设计的,但现实中必须通过代码严格验证,否则就会出现“误删锁”的严重安全问题。
🤔 为什么不能默认“只有持有锁的客户端才能释放”?
因为在分布式系统中,多个服务实例(客户端)都可能尝试去执行 DEL key 操作。如果你不做身份校验,就可能出现以下致命场景:
🔥 危险场景:A 客户端的锁被 B 客户端误删了
- 客户端 A 成功获取锁,设置 lock:stock -> value=A-unique-id,TTL=10秒。
- A 执行业务逻辑耗时较长(比如 15 秒),导致锁在第 10 秒自动过期并被 Redis 删除。
- 此时客户端 B 尝试加锁成功(因为锁已失效),获得同一个锁,设置 lock:stock -> value=B-unique-id。
- A 结束后不知道锁已经过期,仍然调用 DEL lock:stock —— 这时它删除的是 B 的锁!
💥 后果:
B 的锁被提前释放。
其他客户端 C 可以立即进入临界区,与 B 并发执行。
失去了互斥性!造成数据不一致或超卖等严重问题。
✅ 如何防止?→ 加锁时写入唯一标识,解锁前先验证再删除
这就是我们在代码中使用 $this->lockValue = uniqid('', true) . '-' . getmypid(); 的原因。
每次加锁时,把当前进程的唯一 ID 写入 Redis 的 value 中。
解锁时,不能直接 DEL,而要:
lua
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
这段 Lua 脚本的意思是:
“如果这个 key 还存在,并且它的值仍然是我当初设的那个 value,我才允许删除;否则说明锁已经被别人占用或者过期重置了,我不动它。”
这样就能确保:只有真正持有该次锁的客户端,才能释放它。
✅ 问题二:为什么要用 Lua 脚本?
答:为了保证“判断 + 删除”这两个操作的原子性。
🧨 如果不用 Lua 脚本,会发生什么?
假设我们这样写 PHP 代码:
php
// ❌ 错误示范:非原子操作
$currentValue = $redis->get($this->lockKey);
if ($currentValue === $this->lockValue) {
$redis->del($this->lockKey); // ⚠️ 在这一步之前,锁可能已被其他客户端获取!
}
这看似合理,但实际上存在竞态条件(Race Condition):
🔥 场景分析:
- 客户端 A 获取锁,value = "A"。
- A 开始执行上面三行代码:
第一步:GET lock:xxx → 得到 "A" ✅
此时时间片切换,A 暂停。 - 锁到期,Redis 自动删除 key。
- 客户端 B 成功获取锁,设置 lock:xxx -> "B"。
- 时间片切回 A,继续执行第三步:DEL lock:xxx —— 结果把 B 的锁删了!!!
⚠️ 根本问题在于:GET 和 DEL 是两个独立命令,在它们之间有“时间窗口”,其他客户端可以插入操作。
✅ 解决方案:使用 Lua 脚本
Redis 保证:一个 Lua 脚本中的所有命令会作为一个整体执行,期间不会被其他命令打断(单线程模型)。
所以我们用 Lua 实现:
lua
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
这个脚本在 Redis 内部一次性执行完毕,相当于“原子地完成:比较并删除”。
类似于数据库中的 CAS(Compare And Swap)操作。
✅ 总结对比表
方式 是否安全 原因
直接 DEL $key ❌ 不安全 任何客户端都能删,极易误删
PHP 中 get + if + del ❌ 不安全 非原子操作,存在竞态窗口
Lua 脚本 get+del 原子执行 ✅ 安全 原子性保障,只有持锁者可释放
💡 补充建议
$this->lockValue 要足够唯一:可以用 UUID、hostname+pid+timestamp 等组合。
避免长时间持有锁:业务逻辑尽量轻量,防止锁过期后还继续操作。
考虑使用看门狗机制(Watchdog):如 Redisson 的自动续期功能,防止锁因超时而提前释放。
✅ 最佳实践口诀
“谁加锁,谁解锁;解锁必验证;验证须原子。”
而这三点,正是我们使用 唯一 value + Lua 脚本 的根本原因。
我们用最通俗的大白话和生活中的例子,来彻底讲明白“为什么分布式锁要使用 Lua 脚本”。
🧍♂️ 场景比喻:银行保险柜
想象你是一个客户(客户端 A),去银行租了一个保险柜,柜子编号是 888。
银行规定:
每个柜子只能一个人用。
你要在柜子上贴一张写有你名字的纸条(比如:“张三”)。
别人看到这张纸条就知道这个柜子有人用了。
当你用完后,要去把这张纸条撕掉。
❌ 问题来了:怎么确保是你自己撕的?
假设银行没有规定“必须本人撕”,那会发生什么?
情况一:正常流程
- 张三租了柜子888,贴上纸条:“张三”。
- 用完后,张三走过去,看到上面写着“张三”,确认是自己的,就撕掉了。
✅ 没问题。
情况二:出事了!
- 张三租了柜子888,贴上纸条:“张三”。
- 但张三办事特别慢(比如打电话聊了好久)。
- 银行规定:纸条最多贴10分钟。时间一到,自动作废并撕掉。
- 10分钟后,纸条被清除。
- 李四立刻租下同一个柜子888,贴上新纸条:“李四”。
- 这时张三终于办完事,走过来一看:“咦?上面还是‘张三’吗?”
他没注意已经过了10分钟,也看不到现在是“李四”。
他直接动手——把纸条撕了!
💥 结果是什么?
李四的柜子没了!
王五马上就能租这个空柜子……
多个人同时用了同一个柜子!!!
这就是大问题 —— 张三以为自己还在用柜子,其实早就过期了,结果误删了别人的使用权!
✅ 怎么解决?加一条规则!
银行新增一条铁律:
“你要撕纸条前,必须先看清楚上面写的是不是你的名字。如果是,才能撕;如果不是,就不许动!”
但这还不够!还得保证一件事:
你看和撕这两个动作,必须一口气完成,不能中间停顿!
否则又会出问题。
⚠️ 新问题:看和撕之间被人插队
再来看一个危险场景:
- 张三准备退柜。
- 他走过去,看了一眼:“哦,上面写着‘张三’,是我!” ✅
- 但他刚想动手撕,突然手机响了,他接了个电话……📱
- 就在这几秒钟:
柜子过期 → 被清空 →
李四租下柜子 → 贴上“李四” - 张三打完电话,继续动作:“刚才看了是我,现在直接撕!”
- 于是他把“李四”的纸条撕了!
💥 又闯祸了!
虽然张三“看过”是对的,但“看”和“撕”不是连贯动作,中间有空档,别人就趁机插了进来。
✅ 终极解决方案:让银行工作人员帮你“原子操作”
这时候你就该请银行职员帮忙了。
你说:
“请帮我做一件事:如果柜子888上写的字是‘张三’,那就把它撕掉;否则什么都别做。”
银行职员答应后,会一气呵成地完成“看 + 判断 + 撕”这三个步骤,中间不被打断。
这样就不会出错!
🔁 对应回技术:Lua 脚本就是那个“银行职员”
在 Redis 中:
你的 PHP 代码就像你自己去银行办事。
直接用 GET 再 DEL,就像你自己先看一眼再撕,中间可能出错。
而 Lua 脚本 是你交给 Redis 的一段指令,Redis 会:
“我保证一次性、不间断地执行完这段脚本,期间谁也不能插队。”
所以 Lua 脚本的作用就是:
把“检查是不是我的锁”和“删除锁”这两步合成一步,变成一个不可分割的动作。
这就是所谓的“原子性”。
✅ 总结一句话(超简单版)
为什么要用 Lua 脚本?
因为:怕你看着锁是你的,动手时却变成别人的了。
所以让你“边看边撕”,不让任何人插队。
Lua 脚本就是帮你“边看边撕”的那个靠谱助手。
🎯 最终结论
你想做的事 是否安全 说明
直接 DEL key ❌ 不安全 不管是谁的都删
先 GET 再 DEL(两步) ❌ 不安全 中间可能变天
用 Lua 脚本“比较并删除” ✅ 安全 一步到位,只删属于自己的锁
希望这次真的把你讲明白了 😊
记住:“看清楚再撕,而且要一口气撕!”——这就是 Lua 脚本的意义。

浙公网安备 33010602011771号