redisson源码解析-分布式可重入锁(原理篇)
原创不易,转载请注明出处
文章目录
- 前言
- 1.使用demo
- 2.lock原理
- 3.unlock原理
- 总结
前言
redisson作为redis的客户端,提供了特别丰富的功能,关于redisson各种功能使用文档可以看下它的官网或者github,它还提供了基于redis各种分布式锁的实现,比如说重入锁,读写锁,公平锁,RedLock,Semaphore等等,在redis单机,哨兵,集群模式下使用都是ok的,让人感觉就是一个字,强大。本文主要是介绍下redisson分布式可重入锁的实现原理。
1.使用demo
我这里使用的redis环境是主从集群,直接贴出demo代码
public class RedisLock {
private static final Config config ;
private static final RedissonClient redisson ;
static {
config = new Config();
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7002/0")
.addNodeAddress("redis://127.0.0.1:7005/0")
.addNodeAddress("redis://127.0.0.1:7001/0")
.addNodeAddress("redis://127.0.0.1:7004/0")
.addNodeAddress("redis://127.0.0.1:7000/0")
.addNodeAddress("redis://127.0.0.1:7003/0")
.setPassword("123456")
.setConnectTimeout(1000)
.setTimeout(1000);
redisson = Redisson.create(config);
}
public static void main(String[] args) throws InterruptedException {
RLock lock = redisson.getLock("lock_name");
try {
// 加锁
lock.lock();
// 业务逻辑
}finally {
//解锁
lock.unlock();
}
}
}
这段代码首先是创建一个Config 配置类,设置使用redis集群配置,然后就把你那堆redis集群地址搞进去就可以了,它不光支持集群,还支持单机,哨兵等等。
接着就是创建一个redisson客户端了。
main方法里面就是使用redisson客户端创建一个RLock其实就是一个redis锁,然后使用lock 方法加锁,使用unlock方法解锁。
2.lock原理
加锁的时候会根据你的锁名字,比如说上面demo的“lock_name”计算出来属于redis集群中的哪个槽(redis集群槽16384个),然后从集群中找出这个槽所在的master机器,接着就是根据clientId(这个就是创建客户端的时候的一个UUID)拼接上线程id生成一个key。接着就是使用一段lua脚本来加锁了,内部是使用的hash结构,key是锁名字,然后对应的map key就是clientId+线程id,map value就是1,这个1是与可重入有关的,还会设置过期时间,默认是30s。
我把这段加锁lua脚本弄出来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) thenredis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call(‘pttl’, KEYS[1]);
稍微解释一下,KEYS[1] 这个东西就是你的锁名字,也就是“lock_name”,ARGV[2] 就是你的clientId+线程id,ARGV[1]是过期时间默认是30s, 这些应该可以看懂把,exists , hset ,pexpire都是redis的一些命令
如果exists 锁name
是0 ,也就是不存在,就会执行hset 锁name clientId+线程id 1
这个命令与pexpire 锁name 30000ms
(这个就是加过期时间),最后返回null,首次加锁逻辑就是这样的。
重入锁的话就会走下面这个if,如果已经存在了,就自增1,重置一下过期时间是30s,最后return null。
加锁lua脚本调用成功之后,它会创建一个定时任务(watchdog 看门狗),每10s执行一次,不断为这个锁续期,看看如果这个锁还存在的话,就重置一下过期时间为30。
续期lua脚本if (redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1) then
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end;
return 0;
KEYS[1] 还是那个 锁名字,ARGV[2]是clientId+线程id ARGV[1] 是过期时间。
如果这个锁还存在,就会为它重置一下过期时间,然后return 1。
- 这个时候其他线程或者是其他客户端尝试获取这个redis分布式锁,就会失败,然后就会返回一个ttl,这个ttl就是过期时间,其实就是上面lua脚本中
return redis.call('pttl', KEYS[1]);
这行命令产生的。它就会循环等待这个ttl时间过后,然后再尝试加下锁,如果不行再返回ttl,这个时候再等待,就是这样一直循环。
3.unlock原理
unlock其实跟加锁逻辑差不多,会根据你的锁名字,比如说上面demo的“lock_name”计算出来属于redis集群中的哪个槽(redis集群槽16384个),然后从集群中找出这个槽所在的master机器,接着就是根据clientId(这个就是创建客户端的时候的一个UUID)拼接上线程id生成一个key。接着就是使用一段lua脚本来解锁了。
if (redis.call(‘exists’, KEYS[1]) == 0) then
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
if (redis.call(‘hexists’, KEYS[1], ARGV[3]) == 0) thenreturn nil;
end;
local counter = redis.call(‘hincrby’, KEYS[1], ARGV[3], -1);
if (counter > 0) thenredis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
KEYS[1]是锁名字
KEYS[2]是 redisson_lock__channel:{锁名字} 这么一个东西,他其实也是个key,可以理解为主题, 发布订阅用的
ARGV[1]是解锁的标识符
ARGV[2] 过期时间
ARGV[3] clientId+线程Id
第一个第二个 if 我们先不看,看下下面的先是进行-1 ,然后就会返回剩余的counter值。如果这个counter 是大于0的,说明加锁不止一次,也就是加了重入锁,这个时候就会重置一下过期时间。
如果是counter小于等于0的话,就说明这个锁要释放了,这个时候就会删除这个key,并发布一个锁销毁的通知给那些订阅这个频道订阅者。
- unlock lua脚本成功之后,就会取消那个定时任务(watchdog)。
总结
可以看到redisson 基于redis集群模式实现的分布式可重入锁是使用的hash数据结构,然后key就是你设置的那个锁名字,map key是客户端某个线程的唯一标识, map value就是重入次数。不管是加锁,重置失效时间,还是解锁,都是使用的lua脚本来做的。
还没有评论,来说两句吧...