Redis分布式锁之:RedissonLock

Redis提供的分布式锁有多个,这篇笔记主要记录redissonLock的相关内容

是什么

redissonLock的加锁思想:

  1. 在加锁的时候,只能指定加锁时长,不能指定等待时间,这是redissonLock和redLock的一个区别点,需要注意
  2. 如果没有指定leaseTime(加锁时长),默认会加30S,每10S进行一次锁续期
  3. 如果指定了leaseTime,那不会自动续期,到了leaseTime之后,如果依旧没有执行完毕,会释放锁,此时,其他线程就可以继续对key加锁
  4. 如果线程A加锁成功,线程B又来加锁,此时线程B会获取到线程A还有多少秒释放锁,然后线程B会通过semaphore信号量来等待一定的时间
  5. 线程B在等待了一定的时间之后,会重新尝试进行加锁,加锁成功,就返回,如果失败,就重复第4步

加锁源码解析

一、这是加锁的最外层判断

/**
* 这里是加锁的逻辑
* leaseTime是加锁时长
*/
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    // 1.尝试加锁,如果加锁成功,返回的是nil,如果加锁失败,会返回当前key对应的锁失效时间
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // lock acquired 如果等于null,就表示加锁成功,return即可
    if (ttl == null) {
        return;
    }

    // 2.加锁失败,订阅对应的channel:channelName = redisson_lock__channel + 加锁的key
    // 这里订阅,是Redis中提供的功能,具体的细节没有看
    RFuture<RedissonLockEntry> future = subscribe(threadId);
    // 这里的interruptibly是调用方法的时候入参进来的,这里应该是阻塞,具体没搞懂
    if (interruptibly) {
        commandExecutor.syncSubscriptionInterrupted(future);
    } else {
        commandExecutor.syncSubscription(future);
    }

    // 这里是一个死循环,除非当前线程加锁成功,直接break
    try {
        while (true) {
            // 3.这里是进行一次加锁,如果线程加锁成功,就中断死循环,取消订阅,并返回
            ttl = tryAcquire(leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                break;
            }

            // waiting for message
            if (ttl >= 0) {
                try {
                    // 4.这里是在依旧加锁失败的时候,进行await,这里可以点进去看下,实际调用的是semaphore的方法,初始化的信号量为0
                    // 也就是说,tryAcquire会阻塞等待
                    future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    if (interruptibly) {
                        throw e;
                    }
                    future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                }
            } else {
                // 5.这里else的场景,暂时没有想到是哪种场景下会进入到这里,有可能返回的是加锁的key的过期时间 -1?
                if (interruptibly) {
                    future.getNow().getLatch().acquire();
                } else {
                    future.getNow().getLatch().acquireUninterruptibly();
                }
            }
        }
    } finally {
    	// 6.在finally中会取消订阅,如果是线程自己尝试加锁成功了,就会取消订阅;
    	// 在锁被释放的时候,会发消息,通知对应的channelName,可以进行加锁了;在订阅者监听到锁被释放的消息之后,会唤醒对应的线程去加锁
        unsubscribe(future, threadId);
    }
}

二、下面来看加锁的核心逻辑


// 这里是尝试加锁的代码
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    // 1.如果指定了超时时间,就进入下面这个分支执行加锁的逻辑
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 2.如果没有指定超时时间,会进入到这里,默认加锁30S
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }

        // lock acquired
        // 3.在加锁成功之后,会开启一个线程,在第10S的时候,进行锁续期
        if (ttlRemaining == null) {
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

上面的代码可以看到:

  1. 如果指定了加锁时长,就会进入最上面的if逻辑
  2. 如果没有指定加锁时长,会默认加30S,加完锁之后,会进入到onComplete()方法,这个方法就是锁续期的逻辑
    这也是为什么自己指定了加锁时长之后,不会自动进行锁续期的原因

三、最底层加锁的lua脚本

// 这里就是真正加锁的lua脚本了
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);

    return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
            "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]);",
            Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

Redis分布式锁,底层采用的是hash结构
key是加锁的值,也就是在调用lock()方法时,指定的key
field是根据当前加锁的线程ID生成的一个值
value是重入次数

我觉得这段代码,是分布式锁的核心所在,为什么说分布式锁会比setnx要优秀,我觉得这段代码是很关键的一部分原因,通过lua脚本实现
这段lua脚本大致的思想是这样的:

  1. 先判断key是否存在,如果key不存在,就加锁,并且设置过期时间
  2. 如果key已经存在,并且当前加锁的key是重入了,那就将key对应的加锁次数加1
  3. 如果key已存在,并且当前加锁的key和线程和已加锁的不一样,无法重入,那就返回当前key的过期时间

四、锁续期

续期的代码是从 org.redisson.RedissonLock#scheduleExpirationRenewal 这个方法调用过来的

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    
    // 锁续期的核心代码,就是下面这个run方法
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            // 上面应该都是一些判断,最终会调用下面这个代码,这里的lua脚本就是续期的
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                // 如果续期成功,会循环调用 
                if (res) {
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
        // internalLockLeaseTime 这个参数是加锁的时间,默认是30S,这里的意思是,会延迟 30 / 3的时间去执行run方法
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

所以:在续期的时候,会在有效期的三分之一时间时,进行续期,续期成功之后,会再次调用

下面是续期的lua脚本

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
            "end; " +
            "return 0;",
            Collections.singletonList(getName()),
            internalLockLeaseTime, getLockName(threadId));
}

释放锁

org.redisson.RedissonLock#unlock
  org.redisson.RedissonLock#unlockAsync(long)
	org.redisson.RedissonLock#unlockInnerAsync
// 这是解锁的lua脚本
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                // 1.如果key为0,就直接发布一个锁释放的消息
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end;" +
                // 2.如果key + 线程id对应的value为0,表示当前线程所加的锁已经被释放,return即可
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                "end; " +
                // 3.如果上面条件都不满足,就将key + threadId对应的value - 1
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                // 3.1 如果-1之后,依旧大于0,表示重入了,重新设置超时时间即可
                "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                // 3.2 否则,表示当前key + threadId已经释放完毕
                "else " +
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; "+
                "end; " +
                "return nil;",
                Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
    }

在解锁的时候,也有几种场景
1.key、field对应的value为0,表示锁已经释放,return nil即可
2.如果不等于0,就减1,如果减1之后的count依旧大于0,表示是锁重入,还有锁未释放,此时将锁实现时间重新设置为指定的阈值(如果没有指定,默认为30S)
3.如果减1之后的值为0,就del即可,然后发布消息,在对应的channel中,发布个unlock消息

这里所说的channel,就是前面加锁时,其他线程在加锁失败之后,所订阅的消息

总结

所以,redissonLock就是这个逻辑,没有waitTime的概念,线程B加锁的时候,如果线程A已经加锁,就会循环一致等待重试

你可能感兴趣的:(Redis,redis,分布式,java)