RedissonMultiLock + RedissonLock部分源码

Redisson有两个类,分别是RedissonMultiLock和RedsissonLock,都可以实现加锁,但是里面的实现逻辑有些差别

RedissonMultiLock

RedissonMultiLock可以指定等待时间,也就是说,假如我指定了等待时间是2S,比如:
1.A线程来加锁,正常去执行业务逻辑
2.B线程也来加锁,此时会加锁失败,那B线程最多等待2S,如果超过了2S还没有获取到分布式锁,那B线程加锁就返回false,表示加锁失败

加锁

RedissonMultiLock和RedissonRedLock有关系
而RedissonRedLock大致的思想是:在加锁的时候,Redis必须是集群配置,加锁的时候,会依次去多台Redis机器上进行加锁,如果一半以上的锁加锁成功了,就认为是加锁成功了,这个点我没有去看代码验证,只是先看了下加锁和解锁的部分细节

org.redisson.RedissonMultiLock#tryLock(long, long, java.util.concurrent.TimeUnit)
在加锁的时候,会调用到这个方法,这里入参的意思分别是:
第一个waitTime是锁等待时间
leaseTime是锁加锁时间
TimeUnit是时间单位

我的理解是:线程A在加锁的时候,会使用leaseTime作为锁失效时间,如果没有指定,就是默认30S,然后进行锁续期;如果指定了,那就使用指定的阈值,并且不会自动化续期
线程B来加锁的时候,如果线程A已经对同一个key进行了加锁,那线程B最多等待waitTime时间,如果到了这个时间,线程B依旧没有获取到锁,那线程B加锁的方法就会返回false,表示加锁失败

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    long newLeaseTime = -1;
    // 1.这里是在指定了超时时间的时候,会将新的超时时间设置为锁等待时间的2倍;这里我没怎么理解为什么要这么做
    if (leaseTime != -1) {
        newLeaseTime = unit.toMillis(waitTime)*2;
    }
    
    long time = System.currentTimeMillis();
    long remainTime = -1;
    // 2.如果waitTime不等于-1,就是指定了等待时间的场景下,将等待时间转换为毫秒
    if (waitTime != -1) {
        remainTime = unit.toMillis(waitTime);
    }

    // 3.这里的计算,会用remainTime / Redis机器的数量;所以:如果我指定的等待时间是2000ms,那这里获取到的lockWaitTime就是666ms
    long lockWaitTime = calcLockWaitTime(remainTime);
    
    int failedLocksLimit = failedLocksLimit();
    List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
    // 4.对所有的Redis加锁机器进行遍历,然后依次去加锁,如果加锁失败的话,会进行一些列的处理;这里加锁失败的处理逻辑我还没有细看,后面再补充
    for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
        RLock lock = iterator.next();
        boolean lockAcquired;
        try {
        	// 4.1 如果等待时间未指定、锁超时时间未指定,那就调用下面这个方法即可
            if (waitTime == -1 && leaseTime == -1) {
                lockAcquired = lock.tryLock();
            } else {
            	// 4.2 反之,就会调用入参了waitTime和leaseTime的方法
                long awaitTime = Math.min(lockWaitTime, remainTime);
                lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
            }
        } catch (RedisResponseTimeoutException e) {
            unlockInner(Arrays.asList(lock));
            lockAcquired = false;
        } catch (Exception e) {
            lockAcquired = false;
        }
        
        if (lockAcquired) {
            acquiredLocks.add(lock);
        } else {
            if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                break;
            }

            if (failedLocksLimit == 0) {
                unlockInner(acquiredLocks);
                if (waitTime == -1 && leaseTime == -1) {
                    return false;
                }
                failedLocksLimit = failedLocksLimit();
                acquiredLocks.clear();
                // reset iterator
                while (iterator.hasPrevious()) {
                    iterator.previous();
                }
            } else {
                failedLocksLimit--;
            }
        }
        
        if (remainTime != -1) {
            remainTime -= (System.currentTimeMillis() - time);
            time = System.currentTimeMillis();
            if (remainTime <= 0) {
                unlockInner(acquiredLocks);
                return false;
            }
        }
    }

    if (leaseTime != -1) {
        List<RFuture<Boolean>> futures = new ArrayList<RFuture<Boolean>>(acquiredLocks.size());
        for (RLock rLock : acquiredLocks) {
            RFuture<Boolean> future = rLock.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
            futures.add(future);
        }
        
        for (RFuture<Boolean> rFuture : futures) {
            rFuture.syncUninterruptibly();
        }
    }
    
    return true;
}

这里有一个点需要注意的是:不存在只指定锁超时时间,不指定等待时间的场景,因为redisson没有提供这样的接口;
但是会存在只指定了等待时间,没有指定超时时间的场景

根据上面源码的判断,如果指定了超时时间,指定了等待时间,那默认的加锁时间是等待时间的2倍,这里不明白原因

// 这是加锁的核心逻辑,这里我们不管前面传过来的加锁时间等待时间是多少,我们就认为等待时间是2S,加锁时间是5S
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    // 1.这里的这个time就是当前加锁的线程如果加锁失败,最多等待的时间;获取到当前时间、当前线程ID
    long time = unit.toMillis(waitTime);
    long current = System.currentTimeMillis();
    final long threadId = Thread.currentThread().getId();
    // 2.这里就是尝试加锁的逻辑,和redissonLock加锁调用的是同一个方法,就不做过多注释;如果返回的ttl不为null,就表示加锁失败
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // lock acquired
    if (ttl == null) {
        return true;
    }
    
    // 3.在第一次加锁失败之后,会判断等待时间是否超过了阈值;如果超过了,就是说等待时间超过了2S,就返回false
    time -= (System.currentTimeMillis() - current);
    if (time <= 0) {
        acquireFailed(threadId);
        return false;
    }
    
    // 4.如果不超过阈值,就订阅对应的channel
    current = System.currentTimeMillis();
    final RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    // 4.1 这里是调用了await方法,跳进去,发现调用的是countDownLatch.await()方法,总之如果await失败,会进行一系列的处理
    if (!await(subscribeFuture, time, TimeUnit.MILLISECONDS)) {
        if (!subscribeFuture.cancel(false)) {
            subscribeFuture.addListener(new FutureListener<RedissonLockEntry>() {
                @Override
                public void operationComplete(Future<RedissonLockEntry> future) throws Exception {
                    if (subscribeFuture.isSuccess()) {
                        unsubscribe(subscribeFuture, threadId);
                    }
                }
            });
        }
        acquireFailed(threadId);
        return false;
    }

    try {
    	// 4.2 在await了之后,再次判断是否超过了等待时间
        time -= (System.currentTimeMillis() - current);
        if (time <= 0) {
            acquireFailed(threadId);
            return false;
        }
    
    	// 5.如果依旧没有达到阈值,就进行尝试加锁,如果加锁成功,则返回true
    	// 如果加锁失败,在time的基础之上,再减去加锁的耗时
        while (true) {
            long currentTime = System.currentTimeMillis();
            ttl = tryAcquire(leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                return true;
            }

            time -= (System.currentTimeMillis() - currentTime);
            if (time <= 0) {
                acquireFailed(threadId);
                return false;
            }

            // waiting for message
            // 6.代码执行到这里,表示依旧没有超过等待的阈值;这里的getLatch()获取到的是semaphore的一个实现类,会调用其UNSAFE.park(false, nanos);方法
            currentTime = System.currentTimeMillis();
            if (ttl >= 0 && ttl < time) {
                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
            }

            time -= (System.currentTimeMillis() - currentTime);
            if (time <= 0) {
                acquireFailed(threadId);
                return false;
            }
        }
    } finally {
        unsubscribe(subscribeFuture, threadId);
    }
//        return get(tryLockAsync(waitTime, leaseTime, unit));
}

RedissonLock

加锁

/**
* 这里是加锁的逻辑
*/
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的方法
                    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);
    }
//        get(lockAsync(leaseTime, unit));
}

redissonLock加锁的思想是这样的:
1.在加锁的时候,指定了超时时间和未指定超时时间的唯一区别是:
如果没有指定超时时间,第一次默认设置过期时间是30S,在第10S的时候,会进行一次锁续期,重新续期为30S;在第10S的时候会再次续期为30S,所以,如果业务系统一直没有执行完成,会不断的自动续期
如果指定了超时时间,就会设置超时时间为指定的阈值,比如设置5S,如果5S之后,业务系统的代码还没有执行完,Redis会自动释放锁

2.如果线程A加锁成功,就会返回null,如果此时线程B接着来加锁,就会加锁失败,加锁失败的时候,返回了线程A加的锁失效时间
3.线程B在拿到失效时间之后,会去订阅对应的key,然后再休眠指定的时间,这里的时间就是失效时间,如果还有4S失效,那就休眠4S
4.如果在这4S之内,线程A释放了锁,就会唤醒线程B去尝试加锁
5.如果线程A在4S之后没有释放锁,那线程B会尝试加一次锁,如果依旧加锁失败,就会再次休眠,休眠时间由返回的锁失效时间决定


// 这里是尝试加锁的代码
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;
}

下面来看下加锁的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是重入次数

这里的lua脚本,有三种场景
1.当前key未加锁,就是第一个if判断,会直接写入一个hash结构,并将value设置为1
2.当前key已经加锁,但是key、field都对应的上,就是所谓的重入,这种情况会将value + 1
3.key已经存在,且线程ID和已经加锁的线程不一样,此时返回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));
}

这段lua脚本的意思是:

释放锁

调用链是这样的

org.redisson.RedissonLock#unlock
  org.redisson.RedissonLock#unlockAsync(long)
	org.redisson.RedissonLock#unlockInnerAsync
// 这是解锁的lua脚本
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "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;",
            Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

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

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

总结

对于两种锁,底层调用的加锁方法是同一个,都是通过lua脚本来加锁的,只是在外层的判断处理不一样

redLock实现了multiLock,redLock是需要多台Redis机器,在加锁的时候,是会遍历去多台Redis主机去加锁,如果一半以上的Redis主机加锁成功,就认为是加锁成功了
multiLock可以指定等待时间和超时时间,等待时间
等待时间是指在加锁的时候,如果已经有线程加锁成功了,那当前线程最多就等待指定的时间,如果等待了一定的时间之后,还没有获取到锁,那就返回false,表示加锁失败;

只是这里有一个点没搞懂,在同时指定了等待时间和超时时间,那锁的超时时间是等待时间的2倍,并不会使用指定的超时时间

对于redissonLock,在加锁的时候,如果为加锁成功,会订阅指定的channel,然后调用semaphore的tryAcquire方法等待一定的时间,然后再尝试加锁,对于redissonLock没有超时的配置,如果加不到锁,会一直尝试休眠、加锁、休眠

猜你喜欢

转载自blog.csdn.net/CPLASF_/article/details/112007145