Redis分布式锁之:RedLock

redLock也是Redis提供的一个分布式锁,和redissonLock有些区别

是什么

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

redLock的思想是:

  1. redLock的使用,需要有奇数台独立部署的Redis节点
  2. 在加锁的时候,会分别去N台节点上加锁,如果半数以上的节点加锁成功,就认为当前线程加锁成功

还有一个点:不管是redLock,还是redissonLock,两者底层都是通过相同的lua脚本来加锁、释放锁的,所以,两者只是外部形态的不同,底层是一样的

Redis分布式锁之:RedLock_第1张图片

redLock是继承了redissonMultiLock,大部分的逻辑,都是在redissonMultiLock中去实现的,所以源码部分,大部分都是RedissonMultiLock

加锁源码

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 如果等待时间未指定、锁超时时间未指定,那就调用下面这个方法即可,这个方法就和redissonLock加锁是一样的
            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;
}

上面这段代码,是redLock加锁的外层逻辑,有几个点需要注意:

  1. 如果同时指定了waitTime和leaseTime,那leaseTime就会变为waitTime * 2, 这个点一定要注意
  2. 如果没有指定waitTime和leaseTime,底层加锁,就和redissonLock加锁的逻辑是一样的
  3. 如果指定了waitTime和leaseTime,会有一段逻辑的处理

二、针对waitTime和leaseTime的逻辑
这个是上面4.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));
}

根据这段代码的注释,就可以看出来,在加锁的时候,多了一个waitTime的判断,也就是说,如果线程B等待的时候,超过了waitTime,就直接return false, 加锁失败,而不是像redissonLock那样,不停的休眠、重试加锁

总结

释放锁的逻辑和redissonLock释放锁的逻辑几乎上是一样的,只是redLock外层多了一个for循环而已,循环所有的Redis节点,分别去每个节点释放锁

总结下,redLock和redissonLock两者的区别

  1. 前者可以指定waitTime等待时间,线程等待时间超过了阈值,就返回false,表示加锁失败;后者不可以指定waitTime,如果线程加锁失败,就while(true),休眠、重试加锁
  2. 前者要求Redis节点独立部署,分别去多个独立节点上进行加锁,半数以上节点加锁成功,就认为成功;后者没有这个要求,集群部署即可,master会把加锁的key同步到slave节点
  3. 前者指定了waitTime和leaseTime之后,leaseTime会变为waitTime的2倍,这是比较容易踩坑的一个点
  4. 两者最底层,都是通过相同的lua脚本去加锁、解锁的

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