Redisson(二):分布式锁——加锁过程

Redisson

这个是一个Redis框架,提供了各种的分布式服务

Redisson的使用

使用也很简单

  • 注入Redisson
  • redisson.getLock:获取锁,获取锁还没有进行上锁,返回一个RLock
  • RLock.lock:进行加锁,本质上还是执行redis的setnx命令,默认30S过期时间
  • RLock.unlock:进行解锁

下面来一个小Demo

public class ControllerOne {

    private Redisson redisson;

    public String doSomething(){
        //锁的名字
        String lockName = "stock-name";
        //使用redisson来进行获取锁,这一步只是获取还没有进行加锁
        RLock lock = redisson.getLock(lockName);
        try{
            String result = "result";
            //进行加锁
            lock.lock();
            //do something
            return result;
        }finally {
            //进行解锁
            lock.unlock();
        }
    }
}

可以看到用起来十分简单,下面就分析一下源码

源码分析

首先认识一下整体的架构和原理是怎样的

其实整体的架构也就是解决了上一章留下的锁续命问题

  • 当一个线程去尝试获取锁的时候,会采用自旋的方式去一致尝试加锁
    • 加锁成功,就会开启一个后台线程,后台线程会每隔10S检查线程是否还持有锁(进行锁续命)
      • 如果还持有,则会延长锁的时间
      • 如果不持有,就不会进行延长
    • 加锁失败,自旋回去继续加锁

认识了原理之后,我们先认识一个概念,LUA脚本

LUA脚本其实是一门脚本语言,Redisson其实就是利用LUA脚本来执行redis命令的,并且Redis在执行LUA脚本时,会当作一个原子性命令去执行

getLock

先看第一条代码
在这里插入图片描述
这一行代码其实就是实例化想要获取的锁,比如锁的key,因为锁也是一个对象(RLock),在面向对象的语言中必须要先进行实例化才能使用

在这里插入图片描述
进入到Redisson里面,它其实就是实例化了一个RedissonLock,并且给了两个参数

  • CommandExecutor:命令执行器
  • name:锁的key

具体的构造方法如下

    public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        //进行实例化父类
        super(commandExecutor, name);
        //装入命令执行器
        this.commandExecutor = commandExecutor;
        //设置锁的默认过期时间,默认30S
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        //获取订阅频道
        this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
    }

从构造方法可以看到,其实所有的配置都在Config类这里
在这里插入图片描述
至此,一把锁就被实例化了,产生了一个RedissonLock对象

但RedissonLock是一个具体实现,对外的接口却是RLock,所以先看看RLock的构造
在这里插入图片描述
可以看到Rlock多继承了Lock与RLockAsync

  • Lock:JDK的concurrent.locks包下的,也就是JDK自身的Lock,让RLock满足了JDK锁的规范,并且Lock在JDK的实现只有ReentrantLock也就是可重入锁,所以RLock是支持可重入的
  • RLockAsync:Redisson进行扩展的功能,添加了异步锁的功能,所以RLock也支持异步锁

再来看看其实现类
在这里插入图片描述
可以看到,Redisson支持的锁都实现了RLock,所以都有可重入、异步的功能

RedissonLock对象

我们先看一下它整体的架构
在这里插入图片描述
可以看到RedissonLock其实是一个继承的演化过程

  • RedissonObject:抽象类,最底层的与Redis进行交互的功能
  • RedissonExpirable:抽象类,拥有RedissonObject功能的同时,自身提供设置过期时间的一些支持
  • RedissonBaseLock:抽象类,拥有RedissonExpirable功能的同时,自身提供续锁续命功能(开启一个线程任务去不断续命)
  • RedissonLock:拥有BaseLock功能,实现了可重入锁(实现了),与服务器连接断开自动移除锁,并且提供一系列锁的服务,异步上锁等

在这里插入图片描述
对于RedissonLock其成员变量
在这里插入图片描述

  • CommandAsyncExecutor:命令异步执行器
  • LockPubSub:订阅的频道(之后会讲这个频道的作用)
  • internalLockLeaseTime:拥有锁的时间

对于RedissonLock的构造方法(tryLock的目的其实就是构造一把锁出来)

只有一个构造方法

    public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        //构造父类
        super(commandExecutor, name);
        //注入命令执行器
        this.commandExecutor = commandExecutor;
        //设置锁的过期时间,上面也截图显示这为30S
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        //注入订阅服务(Redis的订阅功能)
        this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
    }

整体的结构已经看完了,下面就分析一下,lock方法的细节

lock

lock方法其实是重写RLock的

在这里插入图片描述
对外提供的Lock方法有两种

  • 默认的无参lock方法,无参的就是使用默认的过期时间,也就是30S
  • 带有过期时间和时间单位参数的lock方法

具体调用的lock方法,可以看到这是一个private方法,需要给三个参数

  • leaseTime:过期时间
  • TimeUnit:时间单位
  • interruptiably:是否优先响应中断,线程是可以将其他线程设为中断状态(interrupted),而在分布式中,不同实例之间的线程不能通信,所以需要借助Redis的发布订阅功能来实现通信,也就是线程在频道中发布让那个线程interrupted(实现的本质是AQS)
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    	//获取当前线程ID
        long threadId = Thread.currentThread().getId();
    	//尝试进行加锁
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // lock acquired
    	//如果加锁成功,return结束
        if (ttl == null) {
            return;
        }
    	//加锁失败,为了避免空转,通过订阅频道看能不能获取锁
    	//所以要先进行订阅频道
		//订阅频道
        RFuture<RedissonLockEntry> future = subscribe(threadId);
    	//判断是否优先响应中断
        if (interruptibly) {
            //优先响应中断并开启开门狗
            commandExecutor.syncSubscriptionInterrupted(future);
        } else {
            //不优先响应中断,并开启看门狗
            commandExecutor.syncSubscription(future);
        }
		//开始进行自旋加锁
        try {
            //死循环
            while (true) {
                //再次进行加锁
                ttl = tryAcquire(-1, leaseTime, unit, threadId);
                // lock acquired
                //加锁成功,break跳出循环
                if (ttl == null) {
                    break;
                }
				//加锁失败,并且返回的锁的过期时间>=0
                if (ttl >= 0) {
                    try {
                        //锁拥有过期时间,使用AQS来避免空转和判断线程是否被中断了
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        //假设线程处于中断状态,且优先处理中断
                        if (interruptibly) {
                            //抛出异常
                            throw e;
                        }
                        //假设线程处于中断状态,但并不优先处理中断
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    //ttl < 0代表锁已经消失
                    //只需要判断是否优先处理中断
                    if (interruptibly) {
                        future.getNow().getLatch().acquire();
                    } else {
                        future.getNow().getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
            //最终拿到锁就退出订阅的频道
            //进行退订操作
            unsubscribe(future, threadId);
        }
    }

从这源码上,可以看到大概的逻辑如下

  • 尝试进行加锁
    • 假如第一次加锁成功,直接return
    • 假如第一次加锁失败(锁在Redis存在,且不是自己的锁)
      • 订阅频道,猜测可能是为了避免空转
      • 开始自旋去获取锁
        • 每次自旋获取锁成功就直接break跳出自旋循环
        • 每次自旋如果获取锁失败,就会执行future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS)方法
        • 个人猜测那个方法是用来避免空转的
tryAcquire

通过上面的代码可以知道,tryAcquire是去获取锁的,下面就看看这个tryAcquire做了什么

在这里插入图片描述
先去执行了tryAcquireAsync方法,然后再执行get方法

tryAcquireAsync

源码如下

    private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        //定义一个RFuture,其实RFuture可以理解成是客户端,里面可以保存Redis执行命令的结果
        RFuture<Long> ttlRemainingFuture;
        //如果锁的释放时间为-1,代表使用默认值
        if (leaseTime != -1) {
            //执行自定义过期时间去获取锁方法
            //将返回的结果存入RFuture中
            //给到的参数有等待时间、释放时间、时间单位,线程ID和Reids的EVAL命令
            //eval命令就是用来执行lua脚本的
            //这里要注意,Redisson将所有Redis的命令存进了RedisCommands里面
            ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            //如果为-1,代表使用默认的过期时间
            //使用默认的加锁方法
            //同理将结果存入RFuture中
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }
        //处理一下获取的结果
        //lambda表达式,参数为一个BiConsumer接口,消费者接口(没有返回值,有参数,用来对参数进行处理的)
        //并且为BiConsumer,可以提供两个参数进行消费处理(这里给了一个)
        //onComplete方法就是开启了watch dog
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            //这下面就是接口的accept方法逻辑
            //假如e不为空,直接返回
            //代表获取失败,并且是出现了异常!!!
            //因为e是一个异常
            if (e != null) {
                return;
            }
			
            // lock acquired
            //假如没有异常且ttlRemainning为null,代表获取锁成功了!!!
            if (ttlRemaining == null) {
                //判断过期时间是否为默认的,即判断是否为默认配置
                if (leaseTime != -1) {
                    //不是默认配置的话,将过期时间转换为毫秒
                    internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    //如果是默认配置的,开启定时任务重置过期时间
                    scheduleExpirationRenewal(threadId);
                }
            }
            //假如ttlRemainning不为null,代表获取锁失败
        });
        //做完处理后,返回整个获取的结果
        return ttlRemainingFuture;
    }

从这步获取锁的代码中可以看到以下点

  • Redisson对于请求Redis得到的结果都封装与RFuture里面
  • 判断获取锁时有没有给定过期时间
    • 如果没给定过期时间默认为30S
    • 有就给定自定义的
    • 并且都调用tryLockInnerAsync方法获取锁,执行的Redis命令为EVAL
  • 对获取的结果进行处理
    • 并且传入一个BiConsumer接口(二元的消费型接口)
tryLockInnerAsync

这个方法是用来尝试获取锁的,也就是这里执行底层的LUA脚本

终于快看到底层了,我都快裂开了。。。。

    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        //执行LUA脚本去获取锁
        //传入这里的参数为
        //1. waitTime:等待时间,-1代表不会进行等待
        //2. leaseTime:释放锁的时间,即锁的过期时间
        //3. unit:时间单位
        //4. threadId:线程ID
        //5. command:传进来的Redis命令,这里传进来的是EVAL,执行LUA脚本的命令
        return evalWriteAsync(getRawName(), 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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
    }

可以看到这个方法,调用了evalWriteAsync方法,不过从这个方法里面,我们可以提出到重要信息

  • 执行的脚本文件内容
  • 获取当前线程的名称(getLockName方法,可以看到传进来了一个threadId,那就可以判断,锁的名字是根据线程ID来获取的,且这个lockName的组成是由当前线程连接Redis会产生的客户端ID、冒号和threadId组成)
  • 具体执行脚本文件是交由evalWriteAsync方法去做

脚本的内容如下,也就是获取锁执行的命令

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]);

下面来这段脚本到底在干什么的,不过首先我们要认识存储锁的底层是怎样的

存储锁的底层其实是一个hash结构,这个结构可以理解成锁,,比如仓库的所有(其key值其实就是执行tryLock方法给的lockName),而里面的键值对代表是哪个线程拥有了这把锁,存取的是线程用锁清空,键值对的key对应的就是上面所说的getKeyName方法返回的值(组成为clusterID+:+线程ID),而value是与可重入锁相关的,这个线程可以对这把锁进行重入加锁

  • 先执行exist命令,判断锁是否存在(锁是一个hash对象)
    • 如果不存在
      • 执行hincrby命令进行+1,代表重入一次
      • 执行pexpire命令进行设置超时时间,这里是给hash对象的键值对设置超时时间,也就是锁,而不是整个锁容器上过期时间
      • 返回nil,代表加锁成功
  • 如果锁存在
    • 执行hexists判断是不是发生重入(键值对的key是当前clusterID+:+线程ID,代表哪个线程正在使用这把锁)【前面已经看到getKeyName方法,生产的就是锁的key】,如果线程本身还持有那把锁(hash),那就代表发生重入了
      • 执行hincrby命令,代表发生了重入了,重入次数+1
      • 执行pexpire命令进行添加超时时间
      • 返回nill,代表加锁成功,重入加锁成功
  • 假如锁容器存在且对应的锁不存在,代表这个锁已经被人使用了,不能占有,直接返回锁的过期时间,即hash对象的过期时间

这个方法是专门用来执行lua脚本的

    protected <T> RFuture<T> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {
        //创建CommandBatchService,用来批量去执行命令的
        CommandBatchService executorService = createCommandBatchService();
        //通过CommandBatchServic去异步执行脚本
        //并且将客户端存储的结果封装在RFuture中
        RFuture<T> result = executorService.evalWriteAsync(key, codec, evalCommandType, script, keys, params);
        //ComandBatchService的创建
        //假如commandExecutor可以强转成CommandBatchService
        //那么上面创建的CommandBatchService就是由commandExecutor强转成的
        if (commandExecutor instanceof CommandBatchService) {
            return result;
        }
		//
        RPromise<T> r = new RedissonPromise<>();
        RFuture<BatchResult<?>> future = executorService.executeAsync();
        future.onComplete((res, ex) -> {
            if (ex != null) {
                r.tryFailure(ex);
                return;
            }
		//
            r.trySuccess(result.getNow());
        });
        return r;
    }

从这里可以看到,执行LUA脚本的是由CommandBatchService来负责的,并且一般来说,这个CommandBatchService是由注入的CommandExecutor强转而来,也就是本质是由CommandExecutor来执行的!!!

总结一下加锁过程

把加锁的具体细节都看完了,现在也可以知道为什么前面用long变量接收加锁的结果了,前面产生的一些疑问也随之解开了

  • 底层存储锁的本质是一个hash对象,hash对象就代表是一把锁,比如一个仓库的锁,而hash里面的键值对代表就是当前哪个线程获取了这把锁,只能有一个键值对
    • 键值对里面的key就代表哪个线程在拥有锁,组成为:clusterID:线程ID
    • 键值对里面的value就代表该锁发生重入的次数
    • 并且每次重入成功都会延长过期时间
  • 底层使用lua脚本来保证命令的原子性
  • 执行命令是交由CommandBatchService执行的,其本质是CommandExecutor(先进行强转,强转成功就使用CommandExecutor)
  • 加锁失败,返回的是hash的过期时间,也就是锁的过期时间
Watch dog

回到tryAcquireAsync方法中,接下来就是执行ttlRemainingFuture.onComplete方法了(ttlRemainingFuture是封装加锁的结果的)

一直到onComplete方法底层

在这里插入图片描述
在这里插入图片描述
大概逻辑如下

  • 如果请求redis失败,将错误原因传入accept方法,从accept实现的具体细节可以看到,这样就直接return了
  • 如果请求redis成功,进行下一步的开始watchDog
scheduleExpirationRenewal

这个方法就是开启watchDog的

源码如下

    protected void scheduleExpirationRenewal(long threadId) {
        //ExpirationEntry是用来封装被监视的线程的!!!
        ExpirationEntry entry = new ExpirationEntry();
        //EXPIRATION_RENEWAL_MAP容器是用来存储被监视的锁的
        //通过putIfAbsent方法判断容器中是否已经有该锁了
        //getEntryName返回的是entryName,组成为custerId:lockName(tryLock给的锁的名字)
        //从这里也知道了RedissonBaseLock的entryName是用来映射监视锁容器的对应value的
        ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
        //如果容器中已经存在该锁了
        if (oldEntry != null) {
            //往旧锁中添加线程ID
            oldEntry.addThreadId(threadId);
        } else {
            //往新锁中添加线程ID
            entry.addThreadId(threadId);
            try {
                //开启线程去进行锁续命
                renewExpiration();
            } finally {
                //最后判断线程是否被中断
                if (Thread.currentThread().isInterrupted()) {
                    //如果被中断,取消锁续命
                    cancelExpirationRenewal(threadId);
                }
            }
        }
    }

在这里插入图片描述
总结一下

  • Redisson使用了ConcurrentHashMap去存储正在续命的锁,key为clusterId:lockName
  • 在获取锁成功后,如果releaseTime为-1(也就是默认配置),才会去开启WatchDog
  • 使用ExpirationEntry去封装锁的信息,过期时间与拥有该锁的线程ID(LinkeHashMap记录,因为要支持可重入,value代表重入次数)
    • 判断容器中是否已经存在该锁了,存在的话就添加线程ID,在底层的LinkedHashMap中找到对应的value进行自增,key就为线程ID,并且使用synchronic保证线程安全!!!(对可重入锁的实现支持)
    • 不存在的话就往ConcurrentHashMap底层容器去添加该锁,代表该锁正在被监视续命
  • 开启watchDog后,最后一步会判断线程是否被中止了,如果线程被中止了,就会取消监视

看一下ExpirationEntry的底层线程容器

在这里插入图片描述

renewExpiration

该方法就是开启定时任务,也就是WatchDog去进行锁续命的

    private void renewExpiration() {
        //从容器中去获取要被续命的锁
        ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        //容器中没有要续命的锁,直接返回null
        if (ee == null) {
            return;
        }
        //创建定时任务
        //并且执行的时间为30000/3毫秒,也就是10秒后
        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;
                }
                //Redis进行锁续命
                //这个方法的作用其实底层也是去执行LUA脚本
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                //同理去处理Redis续命结果
                future.onComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock " + getRawName() + " expiration", e);
                        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                        return;
                    }
                    //如果成功续命,递归继续创建下一个10S后的任务
                    if (res) {
                        // reschedule itself
                        //递归继续创建下一个10S后的任务
                        renewExpiration();
                    } 
                    //假如有一次失败,那就会取消续命
                    else {
                        cancelExpirationRenewal(null);
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        
        ee.setTimeout(task);
    }

在这里插入图片描述
现在我们就可以看到,大概整个续命流程了

  • 从容器中取出要续命的锁
    • 假如锁没了,就停止续命,所以容器中起到了一个判断是否还要继续续命的作用
    • 假如锁在,就创建一个10S后执行的TimeOut
      • 执行的任务(TimeTask)是再次判断容器里是否还有锁,没锁就直接Reutrn,有锁的话就执行renewExpirationAsync进行锁续命,参数为线程ID(因为在锁中,标识谁拥有这把锁的key为clusterId:threadId)
      • 假如续命成功,递归再次去创建下一个10S后的定时任务
      • 续命失败,就取消定时任务
  • 所以说,这里存储续命的锁,就是一个是否需要进行续命的标识
renewExpirationAsync

这个方法就是真实进行锁续命的,而且从方法名上看,还是一个异步的动作

源码如下

    protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return evalWriteAsync(getRawName(), 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(getRawName()),
                internalLockLeaseTime, getLockName(threadId));
    }

脚本的内容如下

//判断锁是否被人占用,即hash里面的键值对要等于1
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    //对锁进行锁续命,这里是对hash对象进行续命
	redis.call('pexpire', KEYS[1], ARGV[1]);
    return 1; 
    end; 
return 0;

续命的过程大概如下

  • 判断锁是否存在,且是否被人占用(通过hash对象里面的键值对数量是否为1来进行判断)
    • 如果锁存在,且被人占用,那就为该锁进行续命,续命时间为30S(与原来锁释放时间一致)
    • 如果锁不存在,什么都不做

续命的时间是传的参数internalLockLeaseTime(已经看到为30S),也就是默认的锁释放时间,拓展一下,pexpire和expire的作用是一样的,就是重新对key设置过期时间,但pexpire是毫秒为单位,expire是秒为单位

至此整个tryAcquireAsync方法已经看完了,返回到tryAcquire方法,接下来就是执行get方法了

在这里插入图片描述

get方法

在这里插入图片描述
从源码上可以看到,这个方法是来自RedissonObject的,可以用来取出封装在RFuture的结果

这里的结果就是加锁的结果,加锁成功就为nill,加锁失败就为锁的过期时间

future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);

现在回到Lock方法里面。现在我们知道了,tryAcquire方法就是尝试进行加锁并且加锁成功返回nil,加锁失败返回锁的过期时间的

在前面的lock方法中,对于自旋获取锁失败后,会调用该方法,下面就看看这个方法是做什么的

首先这里要注意,future此时是进行了订阅命令的,保存着订阅监听的结果

RFuture接口

这里使用了future,下面我们就对RFuture这个接口分析一下,这个接口是用来表示异步计算的结果,并且其继承了Concurrent的Future接口(也是用来表示多线程异步计算结果的)

在这里插入图片描述
在这里插入图片描述
所以,这里可以理解为RFuture存放了Redis计算的结果

  • getNow:该方法返回没有阻塞的计算结果,假如还没计算出结果(被阻塞了)会返回null
  • join:该方法返回计算结果,返回的是完成的计算结果,如果被阻塞了或还没执行完会抛出异常
源码解析

源码如下

在这里插入图片描述
在这里插入图片描述
从源码中可以看到,其使用了Concurrent的Semaphore对象,这个对象是一个线程同步的辅助类,可以维护当前访问自身的线程个数,而且可以其调用的tryAcquireSharedNanos方法是AQS实现的!!!

也就是说,这里空转的实现本质竟然是AQS,AQS是一个多线程访问的资源的共享框架,而执行方法的作用是共享式地去获取资源,并且优先响应标志位中断

前面已经认识了lock方法里面的参数interruptiably是让给线程加锁的过程中要优先响应中断,从这里就可以看到,优先响应中断的本质是通过AQS来实现的

而且AQS还有一个作用就是,它底层是实现就是自旋+CAS,并且有对避免空转的支持,所以RedissonLock的lock方法优先响应标志位中断和避免空转是通过AQS去实现的,并且这里空转的时间由ttl决定,并且ttl的含义是,如果加锁成功就为Null,如果加锁失败就为锁(hash对象)的过期时间,所以可以认为,这里空转的时间为锁的过期时间

并且这里甚至做了个比较特别的处理,因为一般ttl如果加锁失败,都会返回正数的过期时间,不过从源码上可以看到,是有对ttl为负数的处理的

在这里插入图片描述
而这里对ttl小于0的处理也很简单,因为ttl小于0代表锁并不存在了,那么只需要判断当前线程需不需要优先响应中断即可(也是由AQS去支持实现)

subscribe和unsubscribe

终于走到lock方法最后的unsubscribe了
在这里插入图片描述
从前面可以知道,避免空转的实现是AQS,那么也可以猜测到subscribe和unsubscribe方法大概也是关于AQS的

总结一下

  • Redisson的锁模型
    • 为一个Hash对象,里面的键值对代表哪个线程拥有这把锁(key,组成为clusterId:线程ID),且可重入几次(value)
  • 使用LUA脚本语言来保证Redis命令的原子性
  • 默认的锁过期时间为30S,锁续命的时间也为30S,开启的WatchDog为10S执行一次
  • WatchDog本质上是一个TimeOut对象,任务是一个10S后执行TimeTask,而TimeTask当给锁续命成功后,递归继续创建TimeOut,继续10S后执行TimeTask,所以本质上是一个递归的过程
  • 存储锁的容器是一个ConcurrentHashMap对象,其作用就是判断锁是否已经被释放掉了,容器中存在该锁,WatchDog才能继续进行锁续命,否则会取消续命
  • 对于线程是否优先响应中断和减少线程空转行为,底层的实现是AQS
  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值