互斥性 : 只能有一个竞争者持有锁,这一点要尽可能保证。
抗死锁性 : 避免因为异常永不释放锁,造成死锁。
对称性 : 同一个锁加锁和释放锁必须是同一个竞争者。
可靠性 : 需要有一定的异常处理能力和容灾能力。
其实只要是有互斥性的中间件那么理论上来说都是可以用于分布式锁的实现的。
就拿redis为例,其实用reids实现分布式锁,其中的“锁”就是一个普通的键值对,因为redis核心操作逻辑本身就是单线程串行化的,那么对于键值对的操作天然就具备互斥性。那么我们就可以通过redis命令模拟加锁过程。
set lock xxx nx #模拟加锁过程,只有在键不存在的情况下命令才会成功
del lock #模拟释放锁的过程
但是如果只是这样的话那么如果持有锁的线程发生了异常不能正常释放锁的话,那么就会形成死锁,锁再也不会被释放,其他的竞争再不能获取资源。所以我们应该设置一种兜底策略,即使在锁持有者异常的情况下,所也能够被释放。
set lock xxx nx ex 10 #模拟加锁过程,并且通过键过期时间作为兜底
del lock #模拟释放锁的过程
通过过期时间兜底的策略确实在一定程度上解决了所持有者异常导致的死锁问题但是,新的问题也就随之产生了。
如果某一次任务执行时间,超过了过期时间,锁已经被释放,并且被其他竞争者获取到了锁,但是当前线程并不知道锁已经被释放,在任务完成之后任然前去释放锁,那么此时他释放的就是其他竞争者的锁,这也就导致了,多个竞争者获取到了锁。破坏了互斥性。所以我们还必须在“锁”上标明锁持有者的身份。
set lock [所持有者唯一标识] nx ex 10
get lock
del lock
但是这里get lock 和 del lock 两个操作无法保证原子性所以需要通过Lua脚本来保证这连个操作的原子性。
local release_lock_script = [[
local lock_key = KEYS[1]
local client_id = ARGV[1]
local lock_holder = redis.call("get", lock)
if lock_holder == client_id then
redis.call("del", lock)
return true
else
return false
end
]]
那么通过一系列的策略“锁”上的设计已经告一段落了。
1.等待机制
而加锁有成功的竞争者就一定也有失败的竞争者,那么竞争者在家所失败后应该怎么办呢,当然是等待重试了,
那么重试该怎么实现呢,其实有两中方案,
1.轮询并等待一段时间,比如锁在加锁失败后睡眠100ms然后尝试再次加锁。
2.是监听锁释放事件(通过发布订阅这模式),不过这种方式的实现起来是比较复杂的。所以要看具体的业务场景。
2.超时重试机制
如果我们的客户端在向redis或者其他中间件请求加锁时超时,应该怎么办呢?
这里主要的问题在于当情求超时时我们并不知道加锁是否成功,所以在重试时我们首先要检查加锁是否成功,然后在选着是否需要重新加锁。
3.过期时间续约机制
为什么需要续约,为了防止总有业务不能再锁过期时间内完成的问题,我们可以考虑引入续约机制,也就是在分布式锁快要过期的时候重置一下过期时间,我们可以通过一个开启一个守护线程,让其在锁快要过期的时候帮助主线程完成锁过期续约。
4.中断业务
如果因为网络延迟等原因导致总是续约失败,那么我们可以考虑通过守护线程中断主线程任务,从而避免多个竞争者持有资源的情况。
5.可重入锁
其实分布式可重入锁就是通过中间件模拟单机可重入锁的加锁和释放锁的过程。那么用redis的hash对象来模拟该过程。
KEYS1 = 「lock」, ARGV「1000,uuid」
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end ;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end ;
return 0;
-- 判断 hash set 可重入 key 的值是否等于 0
-- 如果为 0 代表 该可重入 key 不存在
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
return nil;
end ;
-- 计算当前可重入次数
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
-- 小于等于 0 代表可以解锁
if (counter > 0) then
return 0;
else
redis.call('del', KEYS[1]);
return 1;
end ;
return nil;