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

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!

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 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 RFuture tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {

//定义一个RFuture,其实RFuture可以理解成是客户端,里面可以保存Redis执行命令的结果

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

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

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

总结

这个月马上就又要过去了,还在找工作的小伙伴要做好准备了,小编整理了大厂java程序员面试涉及到的绝大部分面试题及答案,希望能帮助到大家

在这里插入图片描述

在这里插入图片描述

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!

源码如下

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

总结

这个月马上就又要过去了,还在找工作的小伙伴要做好准备了,小编整理了大厂java程序员面试涉及到的绝大部分面试题及答案,希望能帮助到大家

[外链图片转存中…(img-PSBx53As-1714741016782)]

[外链图片转存中…(img-5QQ6mnmZ-1714741016783)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!

  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值