最新Redisson(二):分布式锁——加锁过程,高级java面试题大全

结尾

查漏补缺:Java岗 千+道面试题Java基础+全家桶+容器+反射+异常等

这不止是一份面试清单,更是一种”被期望的责任“,因为有无数个待面试者,希望从这篇文章中,找出通往期望公司的”钥匙“,所以上面每道选题都是结合我自身的经验于千万个面试题中经过艰辛的两周,一个题一个题筛选出来再次对好答案和格式做出来的,面试的答案也是再三斟酌,深怕误人子弟是小,影响他人仕途才是大过,也希望您能把这篇文章分享给更多的朋友,让他帮助更多的人,帮助他人,快乐自己,最后,感谢您的阅读。

由于细节内容实在太多啦,在这里我花了两周的时间把这些答案整理成一份文档了,在这里只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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脚本

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

RFuture tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand 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 RFuture evalWriteAsync(String key, Codec codec, RedisCommand evalCommandType, String script, List keys, Object… params) {

//创建CommandBatchService,用来批量去执行命令的

CommandBatchService executorService = createCommandBatchService();

//通过CommandBatchServic去异步执行脚本

//并且将客户端存储的结果封装在RFuture中

RFuture result = executorService.evalWriteAsync(key, codec, evalCommandType, script, keys, params);

//ComandBatchService的创建

//假如commandExecutor可以强转成CommandBatchService

//那么上面创建的CommandBatchService就是由commandExecutor强转成的

if (commandExecutor instanceof CommandBatchService) {

return result;

}

//

RPromise 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 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 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

最后

小编在这里分享些我自己平时的学习资料,由于篇幅限制,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

开源分享:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】

程序员代码面试指南 IT名企算法与数据结构题目最优解

这是” 本程序员面试宝典!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现。针对当前程序员面试缺乏权威题目汇总这一-痛点, 本书选取将近200道真实出现过的经典代码面试题,帮助广“大程序员的面试准备做到万无一失。 “刷”完本书后,你就是“题王”!

image.png

《TCP-IP协议组(第4版)》

本书是介绍TCP/IP协议族的经典图书的最新版本。本书自第1版出版以来,就广受读者欢迎。

本书最新版进行」护元,以体境计算机网络技不的最新发展,全书古有七大部分共30草和7个附录:第一部分介绍一些基本概念和基础底层技术:第二部分介绍网络层协议:第三部分介绍运输层协议;第四部分介绍应用层协议:第五部分介绍下一代协议,即IPv6协议:第六部分介绍网络安全问题:第七部分给出了7个附录。

image.png

Java开发手册(嵩山版)

这个不用多说了,阿里的开发手册,每次更新我都会看,这是8月初最新更新的**(嵩山版)**

image.png

MySQL 8从入门到精通

本书主要内容包括MySQL的安装与配置、数据库的创建、数据表的创建、数据类型和运算符、MySQL 函数、查询数据、数据表的操作(插入、更新与删除数据)、索引、存储过程和函数、视图、触发器、用户管理、数据备份与还原、MySQL 日志、性能优化、MySQL Repl ication、MySQL Workbench、 MySQL Utilities、 MySQL Proxy、PHP操作MySQL数据库和PDO数据库抽象类库等。最后通过3个综合案例的数据库设计,进步讲述 MySQL在实际工作中的应用。

image.png

Spring5高级编程(第5版)

本书涵盖Spring 5的所有内容,如果想要充分利用这一领先的企业级 Java应用程序开发框架的强大功能,本书是最全面的Spring参考和实用指南。

本书第5版涵盖核心的Spring及其与其他领先的Java技术(比如Hibemate JPA 2.Tls、Thymeleaf和WebSocket)的集成。本书的重点是介绍如何使用Java配置类、lambda 表达式、Spring Boot以及反应式编程。同时,将与企业级应用程序开发人员分享一些见解和实际经验,包括远程处理、事务、Web 和表示层,等等。

image.png

JAVA核心知识点+1000道 互联网Java工程师面试题

image.png

image.png

企业IT架构转型之道 阿里巴巴中台战略思想与架构实战

本书讲述了阿里巴巴的技术发展史,同时也是-部互联网技 术架构的实践与发展史。

image.png

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

库抽象类库等。最后通过3个综合案例的数据库设计,进步讲述 MySQL在实际工作中的应用。

[外链图片转存中…(img-lw9I14ff-1715646834486)]

Spring5高级编程(第5版)

本书涵盖Spring 5的所有内容,如果想要充分利用这一领先的企业级 Java应用程序开发框架的强大功能,本书是最全面的Spring参考和实用指南。

本书第5版涵盖核心的Spring及其与其他领先的Java技术(比如Hibemate JPA 2.Tls、Thymeleaf和WebSocket)的集成。本书的重点是介绍如何使用Java配置类、lambda 表达式、Spring Boot以及反应式编程。同时,将与企业级应用程序开发人员分享一些见解和实际经验,包括远程处理、事务、Web 和表示层,等等。

[外链图片转存中…(img-w36N2BXN-1715646834486)]

JAVA核心知识点+1000道 互联网Java工程师面试题

[外链图片转存中…(img-A2jsXwlY-1715646834487)]

[外链图片转存中…(img-J89rGf55-1715646834487)]

企业IT架构转型之道 阿里巴巴中台战略思想与架构实战

本书讲述了阿里巴巴的技术发展史,同时也是-部互联网技 术架构的实践与发展史。

[外链图片转存中…(img-WQSPMaHK-1715646834487)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值