redisson源码解析-分布式可重入锁(原理篇)

约定不等于承诺〃 2023-01-09 15:48 219阅读 0赞

原创不易,转载请注明出处

文章目录

      • 前言
      • 1.使用demo
      • 2.lock原理
      • 3.unlock原理
      • 总结

前言

redisson作为redis的客户端,提供了特别丰富的功能,关于redisson各种功能使用文档可以看下它的官网或者github,它还提供了基于redis各种分布式锁的实现,比如说重入锁,读写锁,公平锁,RedLock,Semaphore等等,在redis单机,哨兵,集群模式下使用都是ok的,让人感觉就是一个字,强大。本文主要是介绍下redisson分布式可重入锁的实现原理。

1.使用demo

我这里使用的redis环境是主从集群,直接贴出demo代码

  1. public class RedisLock {
  2. private static final Config config ;
  3. private static final RedissonClient redisson ;
  4. static {
  5. config = new Config();
  6. config.useClusterServers()
  7. .addNodeAddress("redis://127.0.0.1:7002/0")
  8. .addNodeAddress("redis://127.0.0.1:7005/0")
  9. .addNodeAddress("redis://127.0.0.1:7001/0")
  10. .addNodeAddress("redis://127.0.0.1:7004/0")
  11. .addNodeAddress("redis://127.0.0.1:7000/0")
  12. .addNodeAddress("redis://127.0.0.1:7003/0")
  13. .setPassword("123456")
  14. .setConnectTimeout(1000)
  15. .setTimeout(1000);
  16. redisson = Redisson.create(config);
  17. }
  18. public static void main(String[] args) throws InterruptedException {
  19. RLock lock = redisson.getLock("lock_name");
  20. try {
  21. // 加锁
  22. lock.lock();
  23. // 业务逻辑
  24. }finally {
  25. //解锁
  26. lock.unlock();
  27. }
  28. }
  29. }

这段代码首先是创建一个Config 配置类,设置使用redis集群配置,然后就把你那堆redis集群地址搞进去就可以了,它不光支持集群,还支持单机,哨兵等等。
在这里插入图片描述
接着就是创建一个redisson客户端了。
main方法里面就是使用redisson客户端创建一个RLock其实就是一个redis锁,然后使用lock 方法加锁,使用unlock方法解锁。

2.lock原理

  1. 加锁的时候会根据你的锁名字,比如说上面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

    1. redis.call('hset', KEYS[1], ARGV[2], 1);
    2. redis.call('pexpire', KEYS[1], ARGV[1]);
    3. return nil;

    end;
    if (redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1) then

    1. redis.call('hincrby', KEYS[1], ARGV[2], 1);
    2. redis.call('pexpire', KEYS[1], ARGV[1]);
    3. 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。

  1. 加锁lua脚本调用成功之后,它会创建一个定时任务(watchdog 看门狗),每10s执行一次,不断为这个锁续期,看看如果这个锁还存在的话,就重置一下过期时间为30。
    续期lua脚本

    if (redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1) then

    1. redis.call('pexpire', KEYS[1], ARGV[1]);
    2. return 1;

    end;
    return 0;

KEYS[1] 还是那个 锁名字,ARGV[2]是clientId+线程id ARGV[1] 是过期时间。
如果这个锁还存在,就会为它重置一下过期时间,然后return 1。

  1. 这个时候其他线程或者是其他客户端尝试获取这个redis分布式锁,就会失败,然后就会返回一个ttl,这个ttl就是过期时间,其实就是上面lua脚本中return redis.call('pttl', KEYS[1]);这行命令产生的。它就会循环等待这个ttl时间过后,然后再尝试加下锁,如果不行再返回ttl,这个时候再等待,就是这样一直循环。

3.unlock原理

  1. unlock其实跟加锁逻辑差不多,会根据你的锁名字,比如说上面demo的“lock_name”计算出来属于redis集群中的哪个槽(redis集群槽16384个),然后从集群中找出这个槽所在的master机器,接着就是根据clientId(这个就是创建客户端的时候的一个UUID)拼接上线程id生成一个key。接着就是使用一段lua脚本来解锁了。

    if (redis.call(‘exists’, KEYS[1]) == 0) then

    1. redis.call('publish', KEYS[2], ARGV[1]);
    2. return 1;

    end;
    if (redis.call(‘hexists’, KEYS[1], ARGV[3]) == 0) then

    1. return nil;

    end;
    local counter = redis.call(‘hincrby’, KEYS[1], ARGV[3], -1);
    if (counter > 0) then

    1. redis.call('pexpire', KEYS[1], ARGV[2]);
    2. return 0;

    else

    1. redis.call('del', KEYS[1]);
    2. redis.call('publish', KEYS[2], ARGV[1]);
    3. 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,并发布一个锁销毁的通知给那些订阅这个频道订阅者。

  1. unlock lua脚本成功之后,就会取消那个定时任务(watchdog)。

总结

可以看到redisson 基于redis集群模式实现的分布式可重入锁是使用的hash数据结构,然后key就是你设置的那个锁名字,map key是客户端某个线程的唯一标识, map value就是重入次数。不管是加锁,重置失效时间,还是解锁,都是使用的lua脚本来做的。

发表评论

表情:
评论列表 (有 0 条评论,219人围观)

还没有评论,来说两句吧...

相关阅读