Redisson分布式锁原理浅析

Redisson分布式锁原理浅析


Redisson锁是我们常用的分布式锁,其核心方法就是获取锁对象(getLock)、加锁(lock、tryLock)和释放锁(unlock),下面从锁的初始化、加锁和释放锁三部分分析Redisson锁的原理。

一、初始化

这里我们一般使用Redisson的getLock方法获取RLock锁对象

public RLock getLock(String name) {
    return new RedissonLock(connectionManager.getCommandExecutor(), name);
}

在getLock方法中新建了一个RedissonLock对象,其源码是


public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
    // 父类的构造方法,最终是RedissonObject
    super(commandExecutor, name);
    // 初始化命令执行器
    this.commandExecutor = commandExecutor;
    // 初始化id,用于锁的前缀
    this.id = commandExecutor.getConnectionManager().getId();
    // 初始化锁的默认时间,这里采用的是看门狗的时间,默认30s
    this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
    // 锁的标识
    this.entryName = id + ":" + name;
    // 初始化锁的监听器,用于在锁被占用时,使用Redis的pub/sub功能订阅锁释放消息
    this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}

二、加锁

加锁有lock和tryLock两类方法,区别在于tryLock方法会有一个等待时间,如果超过等待时间未获取到锁就会返回false,表示获取锁失败,而lock方法会一直等待,直到获取到锁,两者的源码相差不大,这里主要分析tryLock方法的源码。
源码分析如下:

@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    long time = unit.toMillis(waitTime);
    long current = System.currentTimeMillis();
    // 获取当前线程id
    long threadId = Thread.currentThread().getId();
    // 获取锁,底层实现是lua脚本,具体参考下面的tryAcquireAsync源码(这部分包括lua脚本加锁和加锁时间续期)
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // 锁获取成功,返回true
    if (ttl == null) {
        return true;
    }

    // 后面是锁获取失败的处理流程
     
 
    time -= System.currentTimeMillis() - current;
    // 等待时间用完,获取锁失败
    if (time <= 0) {
        acquireFailed(threadId);
        return false;
    }
     
    current = System.currentTimeMillis();
    // 订阅锁释放的通知
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    // 剩余时间内未订阅成功
    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;
        }
     
        // 自旋获取锁
        while (true) {
            // 尝试获取锁
            long currentTime = System.currentTimeMillis();
            ttl = tryAcquire(leaseTime, unit, threadId);
            // 获取成功
            if (ttl == null) {
                return true;
            }
             
            // 未获取成功,若等待时间用完,则加锁失败
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(threadId);
                return false;
            }

            // 通过java.util.concurrent包的Semaphore(信号量)挂起线程等待锁释放
            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));
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    //  指定加锁时间
    if (leaseTime != -1) {
        // lua脚本加锁,详细见tryLockInnerAsync源码
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 未指定加锁时间,则异步执行尝试获取锁,加锁时间是默认的30s
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }
 
        // 获取锁成功,开启时间轮任务,用于锁续期
        if (ttlRemaining == null) {
            scheduleExpirationRenewal(threadId);
        }
    });
     
    return ttlRemainingFuture;
}
 
private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    // 获取当前线程的锁续期任务节点
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        // 若当前线程的锁续期任务任务节点已存在,将节点计数加1
        oldEntry.addThreadId(threadId);
    } else {
        // 不存在,说明是新的线程获取锁
        // 将节点计数加1
        entry.addThreadId(threadId);
        // 开始时间轮调度任务
        // 其会每10s执行一次调度任务,给锁续期
        renewExpiration();
    }
}

这里可以看到在不指定锁的过期时间时,Redisson会自动给锁续期,这可以解决业务未执行完锁就释放的问题,但同时也有缺点,若系统出现异常,导致取消锁续期任务未取消且unlock未执行到,就会造成死锁,其他的线程将无法再获取到该锁。在日常编码中,我们更偏向于指定锁的过期时间,保证在异常情况下锁也能够被释放,至于业务未执行完锁就释放的问题,通过合理评估业务耗时,合理设置过期时间即可。
锁每次重入都会将锁续期任务节点的计数加1,是为了在释放锁的时候判断释放几次后将锁续期任务停止。
下面看一下锁存入redis的lua脚本:

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
    // KEYS[1]为 Collections.<Object>singletonList(getName())的第一个元素,即锁名
    // ARGV[1]是internalLockLeaseTime,即为过期时间
    // ARGV[2]是getLockName(threadId),为锁的唯一标识
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
               // 判断锁是否存在
              "if (redis.call('exists', KEYS[1]) == 0) then " +
                  // 锁不存在,通过hash结果进行存储,hash-key是前缀+线程id,value是1,表示第一次加锁
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  // 设置过期时间
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  // 返回null,加锁成功
                  "return nil; " +
              "end; " +
              // 锁已存在,判断是否是当前线程获取到锁
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  // 是当前线程获取到锁,将hash的value加1,表示锁的进入次数加1
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                  // 设置过期时间
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  // 返回null,加锁成功
                  "return nil; " +
              "end; " +
              // 返回锁的过期时间,表示获取锁失败
              "return redis.call('pttl', KEYS[1]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

通过tryLockInnerAsync的源码不难分析Redisson锁的存储结构,Redisson锁采用的是hash结构,Redis 的Key为锁名,也就是初始化时传入的name参数,hash的key为锁的id属性+线程id,hash的value为加锁次数,不难看出Redisson分布式锁是可重入的
综上,加锁的流程如下:
在这里插入图片描述

三、释放锁

释放锁一般在finally代码块里执行unlock方法,核心方法是unlockAsync,其源码如下:

@Override
public RFuture<Void> unlockAsync(long threadId) {
    RPromise<Void> result = new RedissonPromise<Void>();
    // 释放锁
    RFuture<Boolean> future = unlockInnerAsync(threadId);
 
    future.onComplete((opStatus, e) -> {
        if (e != null) {
            // 释放异常,取消锁续期任务
            cancelExpirationRenewal(threadId);
            result.tryFailure(e);
            return;
        }
         
        // 返回为null,表示该线程未拥有该锁,抛出异常
        if (opStatus == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            result.tryFailure(cause);
            return;
        }
         
        // 取消锁续期任务
        cancelExpirationRenewal(threadId);
        result.trySuccess(null);
    });
 
    return result;
}
 
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    //keys[1]]为 Arrays.<Object>asList(getName(), getChannelName())的第一个元素,即锁名
    //keys[2]为Arrays.<Object>asList(getName(), getChannelName())的第二个元素,为锁释放通知的通道,格式为:redisson_lock__channel:{lockName}
    //ARGV[1]为LockPubSub.UNLOCK_MESSAGE,是发布锁释放事件类型
    //ARGV[2]为internalLockLeaseTime,即过期时间
    //ARGV[3]为getLockName(threadId),是锁的唯一标识
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                // 锁不存在,返回null
                "return nil;" +
            "end; " +
            // 加锁次数减1
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            "if (counter > 0) then " +
                // 剩余次数大于0
                // 设置过期时间,时间为30s
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                // 返回0
                "return 0; " +
            "else " +
                // 剩余次数小于等于0
                // 删除缓存
                "redis.call('del', KEYS[1]); " +
                // 发布锁释放通知
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                // 返回1
                "return 1; "+
            "end; " +
            "return nil;",
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
 
}

释放锁的流程主要是减少redis中锁的次数,若为0则删除缓存,同时减少锁续期任务节点的计数,若为0,则停止锁续期任务,其流程如下:
在这里插入图片描述
以上就是Redisson锁的主要原理。

值得注意的是,如果采用redis-cluster模式,master节点加锁成功,在主从同步时,master节点异常,主从切换,slave节点变成master,此时加锁数据还未同步成功,也就是变成master的slave节点中没有该锁的数据,当其他线程再次对该锁进行加锁操作时,就会有两个线程拥有该锁,会造成并发问题。针对该问题,官方的解决办法是Redlock算法,基本思路是让客户端向多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,则认为,客户端成功地获得分布式锁,否则加锁失败。

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值