redisson版本:3.27.2
简介
锁归根结底就是对同一资源的竞争抢夺,不管是在单体的应用亦或者集群的服务中,上锁都是对同一资源进行修改的操作。
至于分布式锁,那就是多个服务器或资源,同时抢占某一单体应用的同个资源了。在本篇文章中,抢占的资源就是Redis中的某个Key了。
原理
上锁
RLock lock = RedissonClient.getLock("test-lock");
lock.lock();
执行lock.lock()
后,最终会在Redis中执行一段lua脚本。来判断锁是否已经被占用:
if ((redis.call('exists', KEYS[1]) == 0)
or (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]);
参数:
名称 | 内容 |
---|---|
KEY[1] | 锁名称 |
ARGV[1] | 锁过期时间,毫秒 |
ARGV[2] | 锁对象ID+当前线程ID |
注意,在lua
脚本中,上锁时并非设置一个key-value,而是使用了hash结构。
redis.call(‘hincrby’, KEYS[1], ARGV[2], 1);
这样做的目的是Redisson
不光实现了分布式锁,还增加了一个特性:可重入。因为单独的键值对无法存储上锁次数,就使用了hash结构。
上锁时redis日志:
10:40:50.064 [0 192.168.65.1:34743] "EVAL" "if ((redis.call('exists', KEYS[1]) == 0) or (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]);" "1" "test-lock" "30000" "fcce544e-09e1-48bb-9c90-2c77c75d673f:1"
10:40:50.064 [0 lua] "exists" "test-lock"
10:40:50.064 [0 lua] "hincrby" "test-lock" "fcce544e-09e1-48bb-9c90-2c77c75d673f:1" "1"
10:40:50.064 [0 lua] "pexpire" "test-lock" "30000"
可以很清晰的从日志分析出来,Redisson在给分布式锁上锁时所做的操作。
判断锁是否被占->没有被占,抢占并设置过期时间
那么在第一次抢占不到锁时,Redisson在等待时,会不会做些其他事情呢?
的确,Redisson在等待锁时,还会做一些其他事情,免得在傻傻等待。
等待锁释放
在抢不到锁的时候,Redisson会监听redisson_lock__channel
开头的Channel。
锁释放时,抢占锁的应用会向这个Channel发布一个消息(消息内容为:0)。向正在等待锁释放的应用通知此时锁已经释放了,可以尝试抢占锁了。
在上面的这个例子中,对应的Channel名称为:redisson_lock__channel:{test-method}
,消息内容为:0。
解锁
在抢到锁并且本地逻辑运行完成后,此时就需要解锁让其他应用运行下去了。
RLock lock = RedissonClient.getLock("test-lock");
lock.lock();
执行的lua
脚本
local val = redis.call('get', KEYS[3]);
if val ~= false then
return tonumber(val);
end;
-- 看hash中是否还存在这个键(RLock对象的名称以及线程名称)
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
-- counter:hash中减一后的值
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
redis.call('set', KEYS[3], 0, 'px', ARGV[5]);
return 0;
-- 为0了,说明重入锁的次数都删掉了
else
-- 删除锁对应的redis hash表
redis.call('del', KEYS[1]);
-- 发布当前锁释放的通知
redis.call(ARGV[4], KEYS[2], ARGV[1]);
-- 设置对象对应的 值为1
redis.call('set', KEYS[3], 1, 'px', ARGV[5]);
return 1;
end;
参数:
参数名 | 说明 | 值 |
---|---|---|
KEY[1] | 分布式锁名称 | test-lock |
KEY[2] | redis pub/sub 通道名称 | redisson_lock__channel:{test-lock} |
KEY[3] | 正在解锁操作标识 | redisson_unlock_latch:{test-lock}:96ca7c366fa0a6bda6d39931f2092eb1 |
ARGV[1] | pub/sub 通道值-解锁消息(0) | |
ARGV[2] | 锁过期时间 | |
ARGV[3] | Lock对象对应的锁名称 | d5804b0b-50e4-4d61-a91a-319c2ddb5b1d:1 |
ARGV[4] | PUBLISH | |
ARGV[5] | 正在解锁操作标识KEY对应过期时间 |
解锁时Redis日志:
10:40:50.073 [0 192.168.65.1:34745] "EVAL" "local val = redis.call('get', KEYS[3]); if val ~= false then return tonumber(val);end; if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); redis.call('set', KEYS[3], 0, 'px', ARGV[5]); return 0; else redis.call('del', KEYS[1]); redis.call(ARGV[4], KEYS[2], ARGV[1]); redis.call('set', KEYS[3], 1, 'px', ARGV[5]); return 1; end; " "3" "test-lock" "Redisson_lock__channel:{test-lock}" "Redisson_unlock_latch:{test-lock}:4673449b09ccae99bad2a89c9f0122de" "0" "30000" "fcce544e-09e1-48bb-9c90-2c77c75d673f:1" "PUBLISH" "13500"
10:40:50.073 [0 lua] "get" "Redisson_unlock_latch:{test-lock}:4673449b09ccae99bad2a89c9f0122de"
10:40:50.073 [0 lua] "hexists" "test-lock" "fcce544e-09e1-48bb-9c90-2c77c75d673f:1"
10:40:50.073 [0 lua] "hincrby" "test-lock" "fcce544e-09e1-48bb-9c90-2c77c75d673f:1" "-1"
10:40:50.073 [0 lua] "del" "test-lock"
10:40:50.073 [0 lua] "PUBLISH" "Redisson_lock__channel:{test-lock}" "0"
10:40:50.073 [0 lua] "set" "Redisson_unlock_latch:{test-lock}:4673449b09ccae99bad2a89c9f0122de" "1" "px" "13500"
10:40:50.076 [0 192.168.65.1:34746] "DEL" "Redisson_unlock_latch:{test-lock}:4673449b09ccae99bad2a89c9f0122de"
解锁的lua
脚本比上锁时的脚本有太多的逻辑了,不过还是分为了三块:
- 判断是否有其他线程在解锁,如果有其他线程在同时释放锁时,忽略本次操作
local val = redis.call('get', KEYS[3]);
if val ~= false then
return tonumber(val);
end;
- 判断锁是否已经释放,锁已经释放了,无需操作
-- 看hash中是否还存在这个键(RLock对象的名称以及线程名称)
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
- 给锁的hash结构减一,根据减一后的结果做进一步处理。
-- counter:hash中减一后的值
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
redis.call('set', KEYS[3], 0, 'px', ARGV[5]);
return 0;
-- 为0了,说明重入锁的次数都删掉了
else
-- 删除锁对应的redis hash表
redis.call('del', KEYS[1]);
-- 发布当前锁释放的通知
redis.call(ARGV[4], KEYS[2], ARGV[1]);
-- 设置对象对应的 值为1
redis.call('set', KEYS[3], 1, 'px', ARGV[5]);
return 1;
end;
解锁时,会发布一条消息,通知锁已经释放。
10:40:50.073 [0 lua] “PUBLISH” “redisson_lock__channel:{test-lock}” “0”
方便其他正在等待锁的Redisson应用及时唤醒抢占锁。
其他隐藏配置
Redisson在默认上锁时设置的锁过期时间为30S,与其他Java Redis库不设置过期时间的逻辑相反。
由于Redisson显示声明了锁过期时间,那么他一定会在别的地方去一直延长该时间,否则锁在用着用着就被别人抢占了,
于是Redisson中一个特殊机制就出现了:看门狗机制
至于为什么Redisson要这么做,在他对于这个看门狗过期时间配置项可以得知:
This prevents against infinity locked locks due to Redisson client crash or any other reason when lock can’t be released in proper way.
这可以防止由于Redisson客户端崩溃或任何其他原因导致无法以适当的方式解锁而导致的无限锁定。
看门狗续期脚本:
如果锁还在使用中,那么重置锁的过期时间,否则不做任何操作。
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end ;
return 0;
名称 | 内容 | 值 |
---|---|---|
KEY[1] | 锁名称 | test-lock |
ARGV[1] | 锁过期时间,毫秒 | 30000 |
ARGV[2] | 锁对象ID+当前线程ID | d5804b0b-50e4-4d61-a91a-319c2ddb5b1d:1 |
引用文章
https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers
https://github.com/redisson/redisson/wiki/2.-Configuration/