Redission分布式锁解析

使用分布式锁首先要创建一个Config。Config会设置传输模式transportMode为NIO,

lockWatchdogTimeout为30秒。可以设置为单一节点模式,主从订阅模式,集群模式,哨兵模式。

根据此config,可以创建RedissionClient

// 1.构造redisson实现分布式锁必要的Config
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0);
// 2.构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
// 3.获取锁对象实例(无法保证是按线程的顺序获取到)
RLock rLock = redissonClient.getLock(lockKey);

getLock()方法会创建一个RedissionLock实例,该实例有三个属性

internalLockLeaseTime会被赋值为lockWatchdogTimeout的值
CommandAsyncExecutor 一个异步执行类。
LockPubSub 锁的订阅发布,其中LockPubSub中有一个方法:
protected void onMessage(RedissonLockEntry value, Long message) {
    Runnable runnableToExecute;
        //如果是释放锁,value.getListeners()是一个ConcurrentLinkedQueue<Runnable>,会弹出第一个元素,然后唤醒他。
    if (message.equals(UNLOCK_MESSAGE)) {
        runnableToExecute = (Runnable)value.getListeners().poll();
        if (runnableToExecute != null) {
            runnableToExecute.run();
        }

        value.getLatch().release();
 
    } else if (message.equals(READ_UNLOCK_MESSAGE)) {
        while(true) {
            runnableToExecute = (Runnable)value.getListeners().poll();
            if (runnableToExecute == null) {
                value.getLatch().release(value.getLatch().getQueueLength());
                break;
            }

            runnableToExecute.run();
        }
    }

}

这里已经创建完了一个锁实例,后面要进行获取锁的操作:

try {
    /**
     * 4.尝试获取锁
     * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
     * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
     */
    boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);
    if (res) {
        //成功获得锁,在这里处理业务
    }
} catch (Exception e) {
    throw new RuntimeException("aquire lock fail");
}finally{
    //无论如何, 最后都要解锁
    rLock.unlock();
}

多说一嘴,tryLock的实现类有三个,RedissionLock,RedissionMultiLock(将一组锁当成一个锁来操作),RedissionSpinLock(可重入自旋锁),

这里介绍下RedissionLock的tryLock方法,底层tryLockInnerAsync方法会调用lua脚本:这里都是异步的

"if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', 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]);" 进行调用
  1. 在成功获取到锁的情况下,为了避免业务中对共享资源的操作还未完成,锁就被释放掉了,需要定期(锁失效时间的三分之一)刷新锁失效的时间,这里 Redisson 使用了 Netty 的 TimerTaskTimeout 工具来实现该任务调度。
  2. 获取锁真正执行的命令,Redisson 使用 EVAL 命令执行上面的 Lua 脚本来完成获取锁的操作:
  3. 如果通过 exists 命令发现当前 key 不存在,即锁没被占用,则执行 hset 写入 Hash 类型数据 key:全局锁名称(例如共享资源ID), field:锁实例名称(Redisson客户端ID:线程ID), value:1,并执行 pexpire 对该 key 设置失效时间,返回空值 nil,至此获取锁成功。
  4. 如果通过 hexists 命令发现 Redis 中已经存在当前 key 和 field 的 Hash 数据,说明当前线程之前已经获取到锁,因为这里的锁是可重入的,则执行 hincrby 对当前 key field 的值加一,并重新设置失效时间,返回空值,至此重入获取锁成功。
  5. 最后是锁已被占用的情况,即当前 key 已经存在,但是 Hash 中的 Field 与当前值不同,则执行 pttl 获取锁的剩余存活时间并返回,至此获取锁失败。
tryLockInnerAsync()方法返回的是null时,说明获取到了锁。如果锁的释放时间若未被定义,进行线程的锁续时,利用了netty的定时任务,执行时间为internalLockLeaseTime / 3。 这里说明,如果已经设置了过期时间,便不会触发看门狗机制。
if (ttlRemaining == null) {
    if (leaseTime != -1L) {
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
    } else {
        this.scheduleExpirationRenewal(threadId);
    }
}

至此,获取锁到一段落,若获取到锁,返回成功,若获取不到,首先看获取锁的等待时间是否已经耗尽,如果是,则会

this.acquireFailed(waitTime, unit, threadId);
return false; 

如果不是,当前线程会订阅该key,否则在等待时间耗尽前会一直获取锁,如果锁被其他线程获得,获取过时时间,到时间进行争夺。

do {
    long currentTime = System.currentTimeMillis();
    ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
    if (ttl == null) {
        var16 = true;
        return var16;
    }

    time -= System.currentTimeMillis() - currentTime;
    if (time <= 0L) {
        this.acquireFailed(waitTime, unit, threadId);
        var16 = false;
        return var16;
    }

    currentTime = System.currentTimeMillis();
    if (ttl >= 0L && ttl < time) {
        ((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
    } else {
        ((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
    }

    time -= System.currentTimeMillis() - currentTime;
} while(time > 0L);

这个循环可以自己看明晰,解释一下这个if (ttl >= 0L && ttl < time),如果过期时间大于等待时间,会继续换取,传值为ttl,如果过期时间已经小于等待时间,依然会在过期时间继续抢夺锁,传入值为time。这里会把抢夺的线程加入到抢夺线程的node节点后。在前面的首先抢,这里的node应该就是前面那个ConcurrentLinkedQueue,这里就是cas获取节点状态,获取锁

ok,讲到解锁了,解锁关键的就是一段lua语句:

  1. -- 若锁不存在:则直接广播解锁消息,并返回1

  2. if (redis.call('exists', KEYS[1]) == 0) then

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

  4. return 1;

  5. end;

  6. -- 若锁存在,但唯一标识不匹配:则表明锁被其他线程占用,当前线程不允许解锁其他线程持有的锁

  7. if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then

  8. return nil;

  9. end;

  10. -- 若锁存在,且唯一标识匹配:则先将锁重入计数减1

  11. local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);

  12. if (counter > 0) then

  13. -- 锁重入计数减1后还大于0:表明当前线程持有的锁还有重入,不能进行锁删除操作,但可以友好地帮忙设置下过期时期

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

  15. return 0;

  16. else

  17. -- 锁重入计数已为0:间接表明锁已释放了。直接删除掉锁,并广播解锁消息,去唤醒那些争抢过锁但还处于阻塞中的线程

  18. redis.call('del', KEYS[1]);

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

  20. return 1;

  21. end;

  22. return nil;

在释放锁后,会发布消息,通知其他在等待的线程去争夺锁。

在获取不到锁时,线程会订阅这个锁,然后小于等待时间,会作为一个节点加入到AbstractQueuedSynchronizer同步队列中,在这里,会首先看自己的前一个节点是不是头结点,如果是头结点,说明没有等待节点,可以去尝试获取锁,获取到,返回true,否则会封装节点park等待,待前一个节点唤醒后一个节点。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值