Redisson分布式锁的实现原理
加锁机制
如果该客户端面对的是一个 redis cluster 集群,他首先会根据 hash节点选择一台机器。发送 lua 脚本到 redis 服务器上,脚本如下
加锁机制
如果该客户端面对的是一个 redis cluster 集群,他首先会根据 hash节点选择一台机器。发送 lua 脚本到 redis 服务器上,脚本如下
"if (redis.call('exists',KEYS[1])==0) then "+ --看有没有锁
"redis.call('hset',KEYS[1],ARGV[2],1) ; "+ --无锁 加锁 "redis.call('pexpire',KEYS[1],ARGV[1]) ; "+ "return nil; 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 nil; end ;" + "return
redis.call('pttl',KEYS[1]) ;" --不能加锁,返回锁的时间
lua的作用:保证这段复杂业务逻辑执行的原子性。
lua
的解释:
KEYS[1])
: 加锁的
key
ARGV[1]
:
key
的生存时间,默认为
30
ARGV[2]
: 加锁的客户端
ID (
UUID.randomUUID()
)
+ “:” + threadId
)
第一段
if
判断语句,就是用
“exists myLock”
命令判断一下,如果你要加锁的那个锁
key不存在的话,你就进行加锁。如何加锁呢?很简单,用下面的命令:
hset myLock
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
通过这个命令设置一个
hash
数据结构,这行命令执行后,会出现一个类似下面的数据结构:myLock :{"8743c9c0-0795-4907-87fd-6c719a6b4586:1":1 }
上述就代表
“8743c9c0-0795-4907-87fd-6c719a6b4586:1”
这个客户端对
“myLock”
这个锁
key
完成了加锁。
接着会执行
“pexpire myLock 30000”
命令,设置
myLock
这个锁
key
的生存时间是
30
秒。
锁互斥机制
那么在这个时候,如果客户端
2
来尝试加锁,执行了同样的一段
lua
脚本,会咋样呢?
很简单,第一个
if
判断会执行
“exists myLock”
,发现
myLock
这个锁
key
已经存在了。
接着第二个
if
判断,判断一下,
myLock
锁
key
的
hash
数据结构中,是否包含客户端
2
的
ID
,但是明显不是的,因为那里包含的是客户端1
的
ID
。
所以,客户端
2
会获取到
pttl myLock
返回的一个数字,这个数字代表了
myLock
这个锁
key
的
剩余生存时
间。
比如还剩
15000
毫秒的生存时间。
此时客户端
2
会进入一个
while
循环,不停的尝试加锁
自动延时机制
只要客户端
1
一旦加锁成功,就会启动一个
watch dog
看门狗,
他是一个后台线程,会每隔
10
秒检查一
下
,如果客户端
1
还持有锁
key
,那么就会不断的延长锁
key
的生存时间。
可重入锁机制
第一个
if
判断肯定不成立,
“exists myLock”
会显示锁
key
已经存在了。
第二个
if
判断会成立,因为
myLock
的
hash
数据结构中包含的那个
ID
,就是客户端
1
的那个
ID,也就是 “8743c9c0-0795-4907-87fd-6c719a6b4586:1” 此时就会执行可重入加锁的逻辑,他会用:
incrby myLock
8743c9c0-0795-4907-87fd-6c71a6b4586:1 1
通过这个命令,对客户端
1
的加锁次数,累加
1
。数据结构会变成: myLock :{"8743c9c0-0795-4907-87fd-6c719a6b4586:1":
2
}
释放锁机制
执行 lua 脚本如下:
执行 lua 脚本如下:
#如果key已经不存在,说明已经被解锁,直接发布(publish)redis消息
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
# key和field不匹配,说明当前客户端线程没有持有锁,不能主动解锁。 不是我加的锁 不能解锁
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
# 将value减1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
# 如果counter>0说明锁在重入,不能删除key
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
# 删除key并且publish 解锁消息
else
redis.call('del', KEYS[1]);
#删除锁
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
– KEYS[1]
:需要加锁的
key
,这里需要是字符串类型。
– KEYS[2]
:
redis
消息的
ChannelName,
一个分布式锁对应唯一的一个
channelName: “redisson_lockchannel
{” + getName() + “}”
– ARGV[1]
:
reids
消息体,这里只需要一个字节的标记就可以,主要标记
redis
的
key
已经解锁,再结合redis的
Subscribe
,能唤醒其他订阅解锁消息的客户端线程申请锁。
– ARGV[2]
:锁的超时时间,防止死锁
– ARGV[3]
:锁的唯一标识,也就是刚才介绍的
id
(
UUID.randomUUID()
)
+ “:” + threadId
如果执行
lock.unlock()
,就可以释放分布式锁,此时的业务逻辑也是非常简单的。其实说白了,就是每次都对myLock
数据结构中的那个加锁次数减
1
。如果发现加锁次数是0
了,说明这个客户端已经不再持有锁了,此时就会用:“del myLock”命令,从
redis
里删除这个
key
。 然后呢,另外的客户端2
就可以尝试完成加锁了。