Redisson分布式锁源码解析

209 阅读5分钟

1 加锁

加锁的方法入口:Redisson的加锁方法有两个,tryLocklock

1.1 tryLock

tryLock的主要流程有两步:

第一步 调用tryAcquire,线程获取到锁返回成功
第二步 如果没有获取到锁,并且等待时间还没过,继续循环拿锁,同时监听锁是否被释放


@Override
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        // 1 调用tryAcquire,尝试获取锁
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return true;
        }

        // 申请锁的耗时如果大于等于最大等待时间,则申请锁失败.
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(threadId);
            return false;
        }

        current = System.currentTimeMillis();

        /**
         * 2.订阅锁释放事件,并通过 await 方法阻塞等待锁释放,有效的解决了无效的锁申请浪费资源的问题:
         * 基于信息量,当锁被其它资源占用时,当前线程通过 Redis 的 channel 订阅锁的释放事件,一旦锁释放会发消息通知待等待的线程进行竞争.
         *
         * 当 this.await 返回 false,说明等待时间已经超出获取锁最大等待时间,取消订阅并返回获取锁失败.
         * 当 this.await 返回 true,进入循环尝试获取锁.
         */
        RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
        // await 方法内部是用 CountDownLatch 来实现阻塞,获取 subscribe 异步执行的结果(应用了 Netty 的 Future)
        if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
            if (!subscribeFuture.cancel(false)) {
                subscribeFuture.onComplete((res, e) -> {
                    if (e == null) {
                        unsubscribe(subscribeFuture, threadId);
                    }
                });
            }
            acquireFailed(threadId);
            return false;
        }

        try {
            // 计算获取锁的总耗时,如果大于等于最大等待时间,则获取锁失败.
            time -= System.currentTimeMillis() - current;
            if (time <= 0) {
                acquireFailed(threadId);
                return false;

              }

            /**
             * 3.收到锁释放的信号后,在最大等待时间之内,循环一次接着一次的尝试获取锁
             * 获取锁成功,则立马返回 true,
             * 若在最大等待时间之内还没获取到锁,则认为获取锁失败,返回 false 结束循环
             */
            while (true) {
                long currentTime = System.currentTimeMillis();

                // 再次尝试获取锁
                ttl = tryAcquire(leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    return true;
                }
                // 超过最大等待时间则返回 false 结束循环,获取锁失败
                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(threadId);
                    return false;
                }

                /**
                 * 6.阻塞等待锁(通过信号量(共享锁)阻塞,等待解锁消息):
                 */
                currentTime = System.currentTimeMillis();
                // 在 min(ttl,等待时间time)内,从Entry的信号量获取一个许可(除非被中断或者一直没有可用的许可)
                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 {
            // 7.无论是否获得锁,都要取消订阅解锁消息
            unsubscribe(subscribeFuture, threadId);
        }
       return get(tryLockAsync(waitTime, leaseTime, unit));
    }

1.2 tryAcquire

tryAcquire 主要调用了tryAcquireAsync 。对于tryLock方法来说,leaseTime != -1, 直接调用tryLockInnerAsync方法获取锁。

// leaseTime 锁的持有时间
// threadId 当前线程的ID
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
    return get(tryAcquireAsync(leaseTime, unit, threadId));
}

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
        // tryLock设置了锁的等待时长,就直接调用tryLockInnerAsync方法获取锁
        if (leaseTime != -1) {
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        
         // lock 的 leaseTime 传入的是 -1 ,所以走下面的这个分支
      
        // 1)尝试加锁 tryLockInnerAsync
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(
        commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), 
        TimeUnit.MILLISECONDS, 
        threadId,
        RedisCommands.EVAL_LONG);
        
        //2)注册监听事件
        ttlRemainingFuture.addListener(new FutureListener<Long>() {
            @Override
            public void operationComplete(Future<Long> future) throws Exception {
                if (!future.isSuccess()) {
                    return;
                }

                Long ttlRemaining = future.getNow();
                
                // 3)获取锁成功的话,给锁延长过期时间
                if (ttlRemaining == null) {
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }

1.3 tryLockInnerAsync

加锁脚本tryLockInnerAsync中,使用Hash结构存储数据

-- 不存在key时
if (redis.call('exists', KEYS[1]) == 0) then 
  -- 创建锁,锁唯一标识 对应的 value设置为1,设置过期时间,返回nil
  redis.call('hincrby', KEYS[1], ARGV[2], 1); 
  redis.call('pexpire', KEYS[1], ARGV[1]); 
  return nil; 
end; 

-- 存在key 并且锁唯一标识匹配
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
  -- 线程重入次数++,重置过期时间,返回nil
  redis.call('hincrby', KEYS[1], ARGV[2], 1); 
  redis.call('pexpire', KEYS[1], ARGV[1]); 
  return nil; 
end; 
-- 存在key 锁唯一标识不匹配的场景,返回锁的过期时间
return redis.call('pttl', KEYS[1]);

参数含义

参数含义
KEYS[1]锁名
ARGV[1]持有锁的有效时间:毫秒
ARGV[2]锁唯一标识:客户端ID(UUID)+线程ID

流程图

image.png

1.3 lock

lock主要实现加锁锁续约。没有手动设置锁过期时长的参数,lock方法调用lockInterruptibly方法来获取锁。lockInterruptibly主要流程有两步

第一步:预设30秒的过期时长,调用tryAcquire 去获取锁 ( tryAcquire 代码请见1.2 tryAcquire)

 对于`lock`方法,`leaseTime == -1`,所以`tryAcquire`里面主要有两个逻辑:
  • 预设30秒的过期时长,然后调用tryLockInnerAsync获取锁,
  • 如果发现拿到锁了, 开启一个监听器,调用scheduleExpirationRenewal不断去刷新该锁的过期时长

第二步: 获取锁失败,循环继续获取锁

public void lock() {
    try {
       // 调用lockInterruptibly
        this.lockInterruptibly();
    } catch (InterruptedException var2) {
        Thread.currentThread().interrupt();
    }
}

public void lockInterruptibly() throws InterruptedException {
   // 调用lockInterruptibly, leaseTime = -1
    this.lockInterruptibly(-1L, (TimeUnit)null);
}

public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
    // 获取当前线程ID
    long threadId = Thread.currentThread().getId();
    
    // 尝试获取锁的剩余时间
    // ttl为空,说明没有线程持有该锁,线程加锁成功,直接返回
    Long ttl = this.tryAcquire(leaseTime, unit, threadId);
    
   // ttl不为空,说明加锁失败
    if (ttl != null) {
        RFuture<RedissonLockEntry> future = this.subscribe(threadId);
        this.commandExecutor.syncSubscription(future);

        try {
            while(true) {
               // 再此尝试获取锁的剩余时间,如果为null, 跳出循环
                ttl = this.tryAcquire(leaseTime, unit, threadId);
                if (ttl == null) {
                    return;
                }

             // waiting for message 如果ttl >= 0 说明有其他线程持有该锁
                if (ttl >= 0L) {
                   // 获取信号量,尝试加锁,设置最大等待市场为ttl 
                   this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                 // 如果ttl小于0(-1 ,-2 ) 说明已经过期,直接获取
                    this.getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            this.unsubscribe(future, threadId);
        }
    }
}

1.4 锁续约scheduleExpirationRenewal

当一个线程持有了一把锁,并且未设置超时时间,Redisson默认配置了leaseTime =30S,开启开启一个定时任务,每internalLockLeaseTime / 3对该锁进行一次续约,直到任务完成再删除锁。 这就是WatchDog实现的基本思路。

private void scheduleExpirationRenewal(final long threadId) {
   // expirationRenewalMap是一个ConcurrentMap,存储标志为"当前线程ID:key名称"的任务
        if (expirationRenewalMap.containsKey(getEntryName())) {
            return;
        }

        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                // 检测锁是否存在的lua脚本,存在的话就用pexpire命令刷新过期时长
                RFuture<Boolean> future = commandExecutor.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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
                
                future.addListener(new FutureListener<Boolean>() {
                    @Override
                    public void operationComplete(Future<Boolean> future) throws Exception {
                        expirationRenewalMap.remove(getEntryName());
                        if (!future.isSuccess()) {
                            log.error("Can't update lock " + getName() + " expiration", future.cause());
                            return;
                        }
                        
                        if (future.getNow()) {
                            // reschedule itself
                            scheduleExpirationRenewal(threadId);
                        }
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

        if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
            task.cancel();
        }
    }


protected String getEntryName() {
    return this.id + ":" + this.getName();
}

1.5 加锁流程

image.png

3 解锁

3.1 unlock

unlock主要调用unlockInnerAsync来释放锁。

public void unlock() {
  // 发起释放锁的命令请求
    Boolean opStatus = (Boolean)this.get(this.unlockInnerAsync(Thread.currentThread().getId()));

    if (opStatus == null) {
        throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + Thread.currentThread().getId());
    } else {
        if (opStatus) {
   
           // 成功释放锁,取消"看门狗"的续时线程
            this.cancelExpirationRenewal();
        }

    }
}

3.2 unlockInnerAsync

unlockInnerAsync 脚本

-- 锁不存在,广播解锁消息,返回1
if (redis.call('exists', KEYS[1]) == 0) then 
    redis.call('publish', KEYS[2], ARGV[1]); 
    return 1; 
    end;
    -- 锁存在,但是唯一标识不匹配,返回nil
 if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then 
    return nil;
    end; 
    -- 重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
    -- 重次数大于0,重置过期时间,返回 0
if (counter > 0) then 
    redis.call('pexpire', KEYS[1], ARGV[2]); 
    return 0; 
else 
    -- 重次数为 <= 0,删除key,并广播解锁消息,返回1
    redis.call('del', KEYS[1]); 
    redis.call('publish', KEYS[2], ARGV[1]); 
    return 1; 
    end; 
return nil;

参数

参数含义
KEYS[1]锁名
KEYS[2]解锁消息PubSub频道:redisson_lock_channel:{myLock}
ARGV[1]固定值0,表示解锁消息
ARGV[2]锁的过期时间;默认值30秒
ARGV[3]锁唯一标识;同加锁流程

流程图

image.png