Redission是Redis官方推荐的客户端,提供了一个RLock的锁,RLock继承自juc的Lock接口,提供了中断,超时,尝试获取锁等操作,支持可重入,互斥等特性。

基本原理

RLock底层使用Redis的Hash作为存储结构,其中Hash的key用于存储锁的名字,Hash的filed用于存储客户端id,filed对应的value是线程重入次数。

客户端id

客户端id是用于区分每个加锁的线程的,由两部分组成: RedissonLock的成员变量id + 当前线程id Thread.currentThread().getId()。 其中id是一个UUID,在每次实例化Redisson对象实例的时候都会创建一个ConnectionManager,该类会在实例化时生成一个UUID(UUID.randomUUID()), 所以对于每一个Redisson在其生命周期中该id都是相同的,区别在于threadId的不同;而不同Redisson对象之间id也不同,这样可以很好的对不同服务中的线程进行区分。

线程重入次数

可以参考Java的重入锁,用于表示该锁当前被递归的次数,该值>=1,如果等于0时该锁会被删除。

加锁

如何实现可重入加锁

为了实现加锁的原子性,Redisson使用Lua脚本的形式进行加锁。 该脚本位于RedissonLock#tryLockInnerAsync中

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]);
复制代码

其中KEYS[1]是Hash的key也就是RLock的名字,ARGV[2]是客户端id,ARGV[1]是Hash的生存时间TTL。 首先判断KEYS[1]对应的Hash是否存在,如果不存在直接创建一个新的Hash,对应的filed值为1也就是说第一次重入次数为1,并且设置超时时间,最终返回null; 如果Hash已经存在且filed为ARGV[2]的项也存在,说明是该线程递归进入该锁,需要对该filed增加一次重入次数,并且更新超时时间,最终返回null; 如果当前锁已经存在,且不是当前线程持有的,就会返回当前锁的TTL。

互斥性实现

每次进行加锁时会返回锁的TTL,如果TTL为null说明加锁成功,直接返回即可,否则说明已有其他线程持有该锁。后续该线程会进行循环去尝试获取锁直到加锁成功。如果使用tryLock则可以在超时时间结束后直接返回。

Lease续约

如果锁设置了持有锁的超时时间,在超时后会进行锁的释放,如果获取锁的时候不指定持有锁的时间,那么默认获取锁30s后超时。为了防止任务没有执行完就释放锁,Redisson使用一个守护线程(看门狗任务)定时刷新(超时时间的 1/3, 默认是10s,也就是每10s续约30s,直到线程自己释放)这个锁超时时间进行续约,也就是只要这个锁被获取了,则力保这个锁一直不超时,除非获取锁的线程主动释放。由于获取到锁和这个续命任务的守护线程是在同一个线程的,当获取锁的线程挂掉了,意味着刷新任务的线程也会停止执行,就不会再刷新锁的超时时间。

解锁

解锁同样使用Lua脚本执行,代码为与#unlockInnerAsync方法:

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) 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]); 
    return 0; 
else 
    redis.call('del', KEYS[1]); 
    redis.call('publish', KEYS[2], ARGV[1]); 
    return 1; 
    end; 
return nil;
复制代码

首先判断锁是否是存在的,如果不存在直接返回nil; 如果该线程持有锁,则对当前的重入值-1,如果计算完后大于0,重新设置超时持有实践返回0; 如果算完不大于0,删除这个Hash,并且进行广播,通知watch dog停止进行刷新,并且 返回1.

优缺点

优点

  1. Redisson 通过 Watch Dog机制可以解决锁的续期问题;
  2. 和Zookeeper相比较,Redisson基于Redis性能更高,适合对性能要求高的场景。
  3. Redisson 实现分布式可重入锁,比原生的 SET mylock userId NX PX milliseconds + lua 实现的效果更好些,虽然基本原理都一样,但是它帮我们屏蔽了内部的执行细节。在等待申请锁资源的进程等待申请锁的实现上也做了一些优化,减少了无效的锁申请,提升了资源的利用率;
  4. Redison实现了jucLock接口,完整的实现了超时,中断,可重入等各种特性。

缺点

  1. Redisson没有办法解决节点宕机问题,不能达到ZK的一致性;
  2. Redison的机制比较复杂,如果对其底层实现不是很熟悉会出现很多预期外的

Redisson红锁实现

Redisson锁并没有解决主从节点切换可能导致重复加锁的问题,即某个客户端在Master节点加锁,此时主节点宕机,由于主从之间异步复制,从节点没有来得及复制,此时选举出新的Master后并没有之前的锁,另外一个客户端要对同一个锁进行操作时可以直接加锁,那么有两个客户端同时持有一把锁,这样锁住的共享资源会被重复读取,造成混乱。

Redisson提供了RedissonRedLock锁实现了RedLock,需要同时使用多个独立的Redis实例分别进行加锁,只有超过一半的锁加锁成功,则认为是成功加锁。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐