解决分布式锁误删的办法
首先,什么是“误删”?
场景模拟:
1. 客户端A 成功获取了锁,并设定锁的自动过期时间为 10秒。
2. 由于某些原因(比如GC垃圾回收、网络延迟),A的业务逻辑执行了15秒才完成。
3. 在第10秒时,锁因为超时时间到了而自动释放了。
4. 此时,客户端B 成功获取了同一个锁。
5. 第15秒,A的任务终于执行完了,它就去执行释放锁的操作。
6. 结果就是:A把B刚持有的锁给删除了! 这就是可怕的误删。
后果非常严重:B以为锁还在自己手里,继续执行业务逻辑,此时可能有客户端C也能拿到锁,导致数据错乱。
解决之道:给锁加上“指纹”
核心思路很简单:谁加的锁,谁才有资格解锁。 不能让任何人都能来释放锁。
具体实现方法是:在每个锁上贴一个唯一的“指纹”(一个唯一标识),释放锁的时候,要检查这个“指纹”是不是自己的。
实现步骤(以Redis为例):
1. 获取锁时,存入唯一标识(指纹)
当客户端获取锁时,不仅在Redis中设置一个键值对(`key=lock_name`),还要在值(value)中存入一个唯一标识。这个标识可以是:
UUID + 线程ID
或者任何能全局唯一代表当前客户端的字符串。
# 命令:SET lock_name unique_client_id NX PX 10000
# 含义:如果lock_name不存在(NX),则设置它的值为"unique_client_id",过期时间为10000毫秒(PX)
2. 释放锁时,先检查再删除(原子操作)
这是最关键的一步!释放锁时,不能直接`DEL lock_name`。必须要做两件事:
1. 获取当前锁的值,看看是不是自己当初设置的`unique_client_id`。
2. 如果是,才执行删除操作。
但注意! 这两步操作必须是原子的,不能分开。否则在检查和删除的间隙,锁可能因过期而被其他客户端获取, again造成误删。
如何实现原子操作?
通常使用 Lua 脚本。因为Lua脚本在Redis中是原子执行的。
// 释放锁的Lua脚本
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`KEYS[1]` 是锁的名字,就是上面的 `lock_name`。
`ARGV[1]` 是你自己客户端的唯一标识,就是上面的 `unique_client_id`。
脚本逻辑:
“如果锁现在的值还等于我的标识,说明锁还是我持有的,那我就删除它;如果不是,说明它已经不属于我了(可能已经过期并被别人获取),那我就返回0,什么都不做。”
总结一下解决方案
1. 加锁 `SET lock_name 1 NX PX 10000` | `SET lock_name <unique_value> NX PX 10000` | 在锁中存入唯一客户端标识(指纹) |
2. 解锁 `DEL lock_name` | 执行一段 Lua 脚本,先比对`<unique_value>`再删除 | 原子地验证“指纹”,确保只能删自己的锁 |
额外的保障:设置合理的过期时间
误删的根本原因是业务执行时间超过了锁的过期时间。因此,除了上述方法,你还应该:
设置一个合理的、安全的过期时间: 充分评估你的业务逻辑最长可能执行多久,并在此基础上增加一些缓冲时间。例如,业务最多执行5秒,那锁的过期时间可以设置为10-15秒。
使用看门狗(Watch Dog)机制(高级): 一些成熟的分布式锁框架(如Redisson)提供了“看门狗”机制。它会在你获取锁成功后,以一个更短的时间间隔(比如过期时间的1/3)去定期检查业务是否还在执行。如果还在执行,就自动延长锁的过期时间,从而从根本上避免锁因业务未执行完而提前过期。
最终答案
要解决分布式锁误删问题,最核心、最必要的做法是:
在释放锁时,通过原子操作(如Lua脚本)验证锁的Value值是否与当前客户端的唯一标识匹配,匹配成功才能删除。
这样就确保了“谁加的锁,谁才能释放”,彻底避免了误删他人锁的风险。