redisson-lock源码探析

分布式锁

一般情况下, 实现redis的分布式锁, 本质都是保证在一段时间内,当前线程对资源的独占.(这个一定时间, 是为了容错性)
http://redis.cn/topics/distlock.html
安全和活性失效保障

  • 最简单的算法只需具备3个特性就可以实现一个最低保障的分布式锁。
    • 安全属性(Safety property): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。
    • 活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取。
    • 活性B(Liveness property B): 容错。 只要大部分Redis节点都活着,客户端就可以获取和释放锁.

单实例redis分布式锁的姿势

单Redis实例实现分布式锁的正确方法
在尝试克服上述单实例设置的限制之前,让我们先讨论一下在这种简单情况下实现分布式锁的正确做法,实际上这是一种可行的方案,尽管存在竞态,结果仍然是可接受的,另外,这里讨论的单实例加锁方法也是分布式加锁算法的基础。

获取锁使用命令:

SET resource_name my_random_value NX PX 30000

这个命令仅在不存在key的时候才能被执行成功(NX选项),并且这个key有一个30秒的自动失效时间(PX属性)。这个key的值是“my_random_value”(一个随机值),这个值在所有的客户端必须是唯一的,所有同一key的获取者(竞争者)这个值都不能一样。

value的值必须是随机数主要是为了更安全的释放锁,释放锁的时候使用脚本告诉Redis:只有key存在并且存储的值和我指定的值一样才能告诉我删除成功。可以通过以下Lua脚本实现:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Redisson lock

redissonLock,实现了java的Lock接口, 可以像操作jdk中的lock接口一样操作分布式锁
RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误.(所以解锁时可能会抛出异常)

使用方法

  • 最常见的方法, 和操作java的lock一样
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
try{
lock.lock();
// 业务代码 
}finally{
lock.unlock();
}

  • 获取锁需要时间等待, 可以加上等待时间
RLock lock = redisson.getLock("anyLock");
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}
  • 其他还有很多好玩的用法 , 比如公平锁 , redlock(分布式redis场景),读写锁,multilock等
    详见https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

redisson分布式锁, 原理

核心方法,就是getlock,lock, unlock, 重点关注RedissonLock 这个实现类.redisson的lock一般都是异步的 , 这个和他实现的connector有关,就和lettuce一样 .
加锁过程使用的是hash结构 , hash中的field是当前加锁的connectionid+线程id,支持可重入(reetrantlock).
类似这种

  • anyLockname
    01291287_业务线程1
    01291287_业务线程2

另外使用了publish ssubscribe 模式监听锁释放. 因为只有抢不到锁的时候, 才去监听他什么时候释放, 方便再去抢.

以下按照顺序讲解

RLock lock = redisson.getLock("anyLock");


    public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
    }

这个参数"anyLock" 就是作为后续hash结构的key.
internalLockLeaseTime 锁的释放时间(就是key的有效期)
pubSub 用于监听

lock.lock()--重头戏

执行流程

  • 当前线程先去获取锁
    • 获取锁成功, 直接返回
    • 获取锁失败 进行如下处理
      • 订阅当前key,并阻塞, 直到锁被释放
      • while(true)循环, 再尝试获取锁, 如果获取成功, 跳出循环直接返回,。
      • 如果获取失败, 那么继续阻塞, 等待锁释放。并重复上一步
      • 跳出循环后,取消订阅。
 private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        //
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId); //获取锁. 如果获取不到 , ttl为空
        // lock acquired
        if (ttl == null) { //表示加锁成功
            return;
        }
        //执行到这, 表示加锁失败
        RFuture<RedissonLockEntry> future = subscribe(threadId); //异步订阅当前key, threadId只有公平锁时候才有用
        if (interruptibly) { //是否支持中断 下面同步执行订阅(其实是有个默认的订阅时间, 超时就会报错, 防止异常或者太久卡死在这)
            commandExecutor.syncSubscriptionInterrupted(future);
        } else {
            commandExecutor.syncSubscription(future);
        }
       //到这里, 说明key被释放了 , 可以抢锁了
        try {
            while (true) {
                ttl = tryAcquire(-1, leaseTime, unit, threadId); // 还是调用之前的方法, 抢锁
                // lock acquired
                if (ttl == null) {  // 成功, 那就中断跳出去
                    break;
                }

                // waiting for message
                if (ttl >= 0) {  //被别人抢走了
                    try {
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else { //<0   //在Redis 2.6和之前版本,如果key不存在或者key存在且无过期时间将返回-1。
                   //  从 Redis 2.8开始,错误返回值发送了如下变化:
                  //  如果key不存在返回-2
                 //     如果key存在且无过期时间返回-1
                    if (interruptibly) {
                        future.getNow().getLatch().acquire();
                    } else {
                        future.getNow().getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {  //没抢到锁 ,就一直在while true里面转圈
            unsubscribe(future, threadId);  // 取消订阅
        }
//        get(lockAsync(leaseTime, unit));
    }

unlock() 解锁

解锁相对简单很多 , 就是判断当前线程是不是持有锁, 如果持有, 那么就减一(可重入特性), 否则就删除锁
注意, 如果当前线程不持有锁(租期太短), 那么锁会被释放
下面贴一段解锁的代码

    /**
     * 解锁
     * @param threadId
     * @return
     * null: 当前线程没有锁
     * 0: 当前线程还持有重入锁
     * 1: 释放完毕
     */
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + // 如果当前线程没持有锁, 或者锁过期了,返回null
                        "return nil;" +
                        "end; " +
                        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + //走到这,确定当前线程持有锁, 对锁减一, (可重入)
                        "if (counter > 0) then " + //  如果还持有锁, 没释放完
                        "redis.call('pexpire', KEYS[1], ARGV[2]); " +  //续期, 延长锁的时间到internalLockLeaseTime
                        "return 0; " + //返回0
                        "else " +
                        "redis.call('del', KEYS[1]); " +  //否则证明锁已经释放完毕, 删除锁
                        "redis.call('publish', KEYS[2], ARGV[1]); " + //推送消息 , 当前锁已经释放
                        "return 1; " + //释放成功,返回1
                        "end; " +
                        "return nil;",

                Arrays.asList(getRawName(), getChannelName()), //  KEYS[1] key   channel name (redisson_lock__channel+key)
                LockPubSub.UNLOCK_MESSAGE, //ARGV[1]  解锁消息  0
                internalLockLeaseTime,  //ARGV[2]  时间
                getLockName(threadId));  // ARGV[3] connectionid+threadid , 对应的是field
    }

后记

上面的lock()比较简单,自己也可以实现。上面贴出来的为了讲解清楚, 细节肯定少。 可以看加了注释的源码
更好玩的是加等待时间的 , 类似这种

boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS)

玩future比较有意思 。

posted @ 2021-11-23 11:48  rudolf_lin  阅读(347)  评论(0编辑  收藏  举报