由于时间有限,后续会慢慢完善及修改,及添加思维导图,大家可以收藏,关注,以便即时收到更新内容,欢迎大家批评指正,会认真修改
目录
1-17 Redisson 红锁RedLock 以及其算法阐述
1-18 Redisson 读写锁ReadWriteLock
1-20 Redisson 闭锁CountDownLatch
1-2 分布式锁的出现
当一个系统里,用户去修改公共资源的时候,那么可能会出现一些问题,如下代码:

第一个用户修改了企业信息,第二个用户也提交修改了信息,但是第一个用户没执行完可能慢了一拍,这个时候第二个用户修改了,那么最终的数据会成为用户1修改的结果,如果高并发用户都来修改信息,最终数据就乱套了,所以这里在分布式系统,在微服务系统,涉及到公共资源,那么一般最好都需要加上分布式锁。
所以我们的目的目标就是要在分布式环境下,保证共享资源被有序的顺序的访问。
演示:构建企业服务集群

为了测试方便,排除权限校验:


通过apipost引入不同参数,来看最终修改的结果是哪个请求的,结果可以发现第一个请求没处理完毕,第二个请求进来,这个时候,最终的值是第一个请求的,而不是第二个请求的。所以这个结果并不是我们所想要的了。
我们目前的场景其实还不够典型,最经典最典型的还是高并发下单扣库存,因为有可能会出现负数,库存到达0以下这样的情况。所以分布式锁是非常有必要的。
1-3 JVM本地锁
synchronized

ReentrantLock(可重入锁)

不管是synchronized还是ReentrantLock,都是基于本地的,单体绝对没问题,使用他们后的吞吐量是会下降的,因为以前是并行的请求,现在是顺序执行,所以一个完成后接着一个,势必吞吐量就会下来了。
那么在高并发的情况之下,以上两种方式是一般来说是绝对不会使用的。
分布式下的本地锁问题
因为他是本地锁,所以他只会影响本地线程,一旦在微服务或者分布式的环境之下,应该说是集群环境下,因为节点都是水平服务复制的,由于负载均衡,同一个接口在不同集群的节点下都会被访问到。如此,那么其他计算机节点的本地JVM是无法被影响的,因为他只能锁自己,锁不住其他服务节点的线程,所以此时还是依然存在共享资源被争抢的问题,这个时候就是服务和服务之间的争抢了。
1-4 分布式锁原理

用户端请求共享资源的之前,会先去争抢一把锁,谁先拿到才能访问共享资源,拿到以后再释放锁。如果抢到了锁,后续其他的请求只能等待释放,释放了以后再次争抢锁。等其他请求全部释放完,这个锁就会消失。
这个锁可以认为是一个令牌token,只有拿到令牌的线程才能够访问这个共享资源,当然,这个token是需要通过技术编码手段来实现的。而且他是互斥锁,有且只有一个。只要有请求争抢到这个token,其他请求必须等到锁释放。
举个例子:我们这的艾鹿薇奢侈品,由于疫情,是不让所有人全进去店里购买的,必须每人排队发放一张卡,有了这张卡才能进店里购买,并且只能进一个人(或一个家庭),直达出来,才能放后面的进入,也就是每次都是一个个的进去,这么这个一张卡其实就是令牌的理念,也就是分布锁了。
其实分布式锁的主要目的其实就是为的让数据达到一致性。让客户端,让进程同步的来访问共享资源,在并发的场景下可以达到一致性。
分布式锁的类型:
- JVM锁(本地锁,集群下失效)
- mysql乐观锁
- mysql悲观锁
- redis 分布式锁
- zookeeper分布式锁
1-5 MySql 悲观锁与乐观锁
悲观锁
select ... for update
在查询的后面加上for update,锁住记录。此时其他用户请求在执行操作的时候,则会被阻塞的;只有在之前用户提交或者回滚以后,才能执行。
这个悲观锁也可以称之为行级锁,他锁住的是行记录,所以他所影响的范围就是行;当然除了行级锁以外还有表级锁,他锁的就是整张表,影响的范围当然是整张表了,当然我们不可能会使用表级锁,因为其他行记录完全不可用,因为全锁了,性能太差了。
悲观锁看似还可以,但是他有一个非常致命的问题,那就是容易造成死锁。那就是对多条记录进行加锁的时候,顺序紊乱了,加锁记录太多了,很容易引发死锁问题。
乐观锁
对需要的数据表增加version字段,提供版本号的支持
- 查询记录,当前version为0
- 更新记录,设置当前记录的version为1(累加1)。但是,更新的sql语句需要添加
where version=0 - 如果当前有很多人都要更新,那么他们都能获得当前的version为0,但是只有1个人可以更新成功,因为成功以后版本号则变为了1,因为他们的自身条件还是
where version=0,则其他人的请求则异常。如此就控制了这条共享资源被同时更新了。
乐观锁虽然也可以控制,但是并发访问的时候会出现大量的错误,所以可能导致后续的请求全部失败,在互联网下单场景是很显然不行的,会造成平台损失大量订单的。当然我们也可以递归去处理乐观锁,但是随着并发的上升,吞吐量会越来越低。
所以说高并发使用乐观锁性能是极低的。
此外。verion是用来控制的,但是verion本身也有可能会被篡改,被篡改就意味着锁失效,可能出现脏数据。就是你再更新的时候,虽然你觉得没问题更新成功了,但是那个version可能是被改了,只是你觉得没问题,但是实际上你入库的数据有问题。 第一个用户更新version变为1了,被别人用户进行篡改成0,后面的请求会被重新进行更新。
这其实就是一个典型的数据库ABA数据不一致问题。
小节
如果我们的共享资源都在数据库中,那么我们完全可以通过数据库锁来实现,但是往往我们在分布式微服务的大环境开发中,所涉及到的共享资源可能还会有mongodb、redis、elasticsearch等。所以数据库锁是不够的。
而且数据库锁的性能也是很差的,对此,我们往往需要采用分布式中间件来实现分布锁这样的机制。
1-6 Redis锁setnx与业务代码处理
redis 的 setnx区别于普通set,他是set key if not exist,当一个key不存在的时候,可以设置成功。那么,我们就可以把setnx来设定某个key为一把锁,这个key存在的时候,则表示获得锁,那么请求无法操作共享资源,除非这个key不存在了,那就行。
如下图:

第一次设置成功,第二次设置不成功,因为这个key没有释放,除非删除了,或者超时清除了,那么才可以。

从上面操作可以看得出来,这其实也是分布式锁的3个关键步骤,加锁设值,删除解锁,重试(死循环或者递归)
通过如下流程可以更好梳理思路:

代码整合:
思考问题:
- 如果业务执行的过程抛出异常了,怎么办?锁会一直没释放。
- 如果当前运行这段代码的计算机节点突然停电了,代码整找准备删除lock,这个时候咋办?锁也会一直存在
1-7 setnx锁超时自动过期
上一节课遗留思考问题:
- 如果业务执行的过程抛出异常了,怎么办?锁会一直没释放。
- 如果当前运行这段代码的计算机节点突然停电了,代码整找准备删除lock,这个时候咋办?锁也会一直存在

由于上一节课提出的两个问题,其实我们要保证锁最终不管怎样都要释放,所以,我们可以为锁添加过期时间,如上图。
一旦后续发生故障,那么30秒后还是能释放锁。
但是这个时候还是会有问题,程序正好运行到图1.1还没来得及设置过期时间,拉电了,此时锁设置成功,但是没有设置过期时间,还是有问题。
所以,要么全设置成功,原子性必须得保证。
我们可以使用 setnx内置的,可以多加时间参数来设置。 
1-8 添加setnx锁请求标识防勿删
每个请求删除的时候,必须只能删除自己的锁,所以在生成锁的时候,创建一个uuid作为标记即可,在删除的时候进行判断就行了。

1-9 递归改造while循环
目前所使用的递归方案,高并发时也容易造成内存溢出,那么其实可以改造一下,改为死循环即可,只要获得锁失败,则返回去尝试获得锁即可。避免原子性问题删除锁放在fianlly里

1-10 LUA 原子性操作
之前的代码思考一下,其实还是有问题的

图中箭头处,当我们拿出锁后,并且判断也成功了,在这一刹那间,锁也可能正好失效吧。这个时候已经进入了判断内部了,所以会执行删除锁,但是这个时候因为锁恰好失效,所以其他请求就占有锁,那么自己在删除锁的时候,其实删除的是别人的锁,这样在极端的情况下其实也会出问题的。此时怎么办?
查询锁并且删除锁,这其实也是原子性操作,因为上一节课说了,这里也是可能会删除其他的锁的,因为原子性保证不了。
所以接下来我们所需要做的,就是保证查询以及判断都是原子性的操作。这里就需要结合使用LUA脚本来解决这个问题。
可以打开redis官网:Commands | Docs


![]()
相当于代码里的判断语句
![]()
解释:get命令获得key与参数比对,如果比对一致,则删除,否则返回0。
这是一段脚本,是一个命令一起运行的,所以要比我们程序代码中的调用要来的更好,因为这是原子性操作。要么全成功,要么全失败。
在命令行可以通过eval命令来进行操作:



EVAL "return ARGV[1]" 3 name age sex lee 18 man 183 200

# redis.call 可以调用redis的相关命令
EVAL "return redis.call('get',KEYS[1])" 1 name
把上述脚本转换为一个字符串(大家可以直接复制):
String lockScript =
" if redis.call('get',KEYS[1]) == ARGV[1] "
+ " then "
+ " return redis.call('del',KEYS[1]) "
+ " else "
+ " return 0 "
+ " end "
;
在通过redis调用即可:


1-11 setnx 锁自动续期
遗留问题思考:
- 我在这里设置了30秒,如果业务执行时间很长,需要35秒,这个时候还没等业务执行完毕就释放锁了,那么其他请求就会进来处理共享资源,那么锁其实就失效了,没起到作用了。
前面我们设置了超时时间,但是如果真的业务执行很耗时,超时了,那么我们应该给他自动续期啊。
redis service其实是多线程的,开启(fork)一个子线程,定时检查,如果lock还在,则在超时时间重置,如此循环,直到业务完成后删除锁。(或者使用while死循环也行)
LUA脚本:
String checkScript =
" if redis.call('get',KEYS[1]) == ARGV[1] "
+ " then "
+ " return redis.call('expire',KEYS[1],30) "
+ " else "
+ " return 0 "
+ " end "
;
转换为字符串去运行:
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('expire',KEYS[1],30) else return 0 end

EVAL "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('expire',KEYS[1],30) else return 0 end" 1 lock abc
运行后,原来永不过期的,现在被附上了30秒时间,表示这段脚本没问题

java代码实现
使用自定义定时线程池,不好控制去关闭。使用定时器工具组件

延迟delay毫秒后,执行第一次task,然后每隔period毫秒执行一次task。

解锁的时候接触定时任务

测试
在业务代码中增加sleep测试
那么执行过程中,会经过几次的续期,结束了,就释放timer。
1-12 Redisson 概述与入门整合

Redisson: Easy Redis Java client and Real-Time Data Platform
https://github.com/redisson/redisson

和Jedis以及RedisTemplate一样,Redisson其实也是redis的一个客户端。
Redisson里面封装了很多有用的api和功能实现,非常实用,当然也包含了分布式锁。Jedis这样的客户端仅仅只是把提供了客户端调用,很多功能其实需要自己去实现封装的。Redisson所提供的是实用redis最简单最便捷的方法,Redisson的宗旨也是让我们使用者关注业务本身,而不是要更关注redis,要把redis这块分离,使得我们的精力更加集中于业务上。
Redisson内部结合实用了LUA脚本实现了分布式锁,并且可以对其做到续约释放等各项功能,非常完善。当然也包含了JUC里面的一些锁,JUC里面的只能在本地实现,集群分布式下则失效,如果要使用则可以使用Redisson提供的工具来实现锁就行了。
入门示例
在api工程加入maven依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.19.0</version>
</dependency>
配置redisson放入容器:

还原service代码:
整合redisson:

上面的代码其实就是设计为可重入锁,不多赘述,简单来讲,就是方法运行,可以多次使用同一把锁。或者说一个线程在不释放的情况下可以获得锁多次,不过在释放的时候也需要释放多次。(有兴趣课后建议去学习一下JUC相关内容)
测试
apipost测试接口最终结果的顺序即可。
1-13 Redisson 分布式锁测试
Redisson 常用基本配置

测试
- 拔电源测试会否解锁
- 自动续期测试(看门狗)
- lock设置自定义时间,比如15秒,超时是否自动续期(无看门狗)
- 测试可重入锁(用同一把锁):重入2次,释放2次,


1-14 Redisson 分布式锁底层源码品读
https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器
思维导图:【分布式】redisson 中lock方法源码思维导图-CSDN博客
lock() 与 tryLock() 的区别
(1)返回值: lock() 是没有返回值的;tryLock() 的返回值是 boolean。
(2)时机:lock() 一直等锁释放,阻塞式的;tryLock() 获取到锁返回true,获取不到锁并直接返回false。非阻塞式的
(3)tryLock是可以被打断的,被中断的;lock是不可以。
线程在调用lock方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加谨慎地申请锁。tryLock方法试图申请一个锁,在成功获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做其他事。
可以调用tryLock时,使用超时参数。
lock方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么,lock方法就无法终止
执行流程

加锁lock()



什么是自旋?
在Java中,自旋是指在多线程编程中,当线程尝试获得某个锁时,如果该锁已经被其他线程占用,线程会一直循环检查该锁是否被释放,直到获取到该锁为止。这个循环等待的过程被称为自旋。比如 while 循环或者 for 循环
while(true)作为无限循环,经常在不知道循环次数的时候使用,并且需要在循环内使用break才会停止。
自旋的主要目的是避免线程切换带来的开销。当线程需要获得某个锁时,如果该锁已经被其他线程占用,线程会进入等待状态,操作系统需要进行线程切换,从而导致一定的开销。如果等待时间很短,那么这种开销是不必要的。在这种情况下,自旋可以避免线程切换,提高程序的性能。
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 尝试加锁,返回值为需要等待的时间,若为空则表示当前线程加锁成功
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
//若没有获取到锁时,会给该线程向channel订阅锁通知,向channel订阅后,锁释放后可以收到通知立即去尝试获取锁;
//订阅监听redis消息,并且创建RedissonLockEntry,其中RedissonLockEntry中比较关键的是一个 Semaphore属性对象,用来控制本地的锁请求的信号量同步,返回的是netty框架的Future实现。
CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
pubSub.timeout(future);
RedissonLockEntry entry;
if (interruptibly) {
entry = commandExecutor.getInterrupted(future);
} else {
entry = commandExecutor.get(future);
}
try {
//while循环继续尝试获取锁
while (true) {
// ttl为null,说明获取成功,返回
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// 若锁有效期ttl大于0,则阻塞ttl后继续获取,防止空自旋(白访问的意思)
//解释:自旋获取并不是说直接无限调用tryAcquire,而是根据上次tryAcquire返回的ttl阻塞等待后才再次tryAcquire
// waiting for message
if (ttl >= 0) {
try {
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
entry.getLatch().acquire();
} else {
entry.getLatch().acquireUninterruptibly();
}
}
}
} finally {
unsubscribe(entry, threadId);
}
// get(lockAsync(leaseTime, unit));
}

进入tryAcquire()→ tryAcquireAsync()方法,该方法首先会判断释放时间是否为-1,如果为-1则代表我们在前面没有传入超时时间,根据这个判断结果将会走两条不同的路。
//方法接受四个参数:waitTime(等待时间)、leaseTime(锁的租期)、unit(时间单位)和threadId(线程ID)。
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
// 判断锁是否有租期,并尝试调用tryLockInnerAsync获取锁得到一个表示锁剩余时间(ttlRemaining)的RFuture对象。
if (leaseTime > 0) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
// 使用内部预设的internalLockLeaseTime作为锁的租期去尝试获取锁。
// 此处获取的是Config#lockWatchdogTimeout的值30*1000也就是默认30秒
ttlRemainingFuture = tryLockInnerAsync(waitTime, this.internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
// 然后通过调用thenApply方法对这个异步结果进行进一步处理:
CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
// ttl剩余时间等于空说明锁拿到了
if (ttlRemaining == null) {
// 判断租期是否大于0
if (leaseTime > 0) {
//将外部传的锁租期时间赋给内部锁的租期时间
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
// 说明这个锁没有租期,则开启一个线程自动为这个锁续期,确保在过期前得以自动续组
scheduleExpirationRenewal(threadId);
}
}
return ttlRemaining;
});
return new CompletableFutureWrapper<>(f);
}
1、第一条路如下图:**首先来看如果我们在前面传入了自定义超时时间,那么将会执行tryLockInnerAsync()方法,可以看到里面是很直接的在和redis通信,将一段lua脚本以及自定义的超时时间等信息传给redis进行占锁。需注意的是这个方法的返回值是Future,这里用到了异步编排,如果占锁成功,会在后面用这个返回值进行监听。

底层本质上就是运行了一个lua脚本:

脚本解读:
- 先通过
exists判断锁是否存在,如果为0则表示锁不存在 - 锁不存在,则创建一个锁并且设置过期时间
- 如果
hexists判断是否存在,判断是自己的锁,则重置过期时间(第二个判断可以理解为锁的重入) - 如果两个判断都不通过,则返回一个pttl的毫秒时间
-- 还没有线程加锁
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
-- 重入次数+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 设置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 锁被其他线程占有,则返回剩余有效期
return redis.call('pttl', KEYS[1]);
2、第二条路如下图:**如果没有传入超时时间,那么不仅会调用tryLockInnerAsync()方法,还会通过一个scheduleExpirationRenawal()的方法进行锁续期。

与第一条路执行同一个lua脚本:

来看一下tryLockInnerAsync()方法在这里传入的参数,当前面没有传入超时时间时,这里传入了一个“internalLockLeaseTime”参数来替代,追踪这个参数值,可以看到在RedissonLock构造时为这个参数赋了值,而这个时间值就是配置中的“看门狗”的时间,这个值为30s。


自动续期,超时时间为30s,每次续期3/1时间
也就是说,即使我们不传入超时时间,Redisson也会自动给我们设置一个默认的超时时间30s。下面再来看神秘的scheduleExpirationRenawal()方法,这是在占锁成功后进行监听过程中涉及到的方法,须注意的是如果我们传入了自定义超时时间是不会执行该方法的。再看这个方法里面有一个重要的方法——renewExpiration(),看名字也能猜出来这是用于重新设置超时时间。进入这个方法,看核心部分可以知道主要是通过一个连接管理器创建了一个定时任务,任务的内容就是用lua脚本和redis通信重新设置超时时间,这个过期时间依然是看门狗的超时时间30s。
此方法内部本质上也是一个timer的定时器,future.whenComplete((res , e)->{})为定时异步编排任务回调函数,e为异常信息,res为定时异步编排任务返回值,返回值为true就是lua脚本返回1,则继续递归,false则取消

脚本解读:
- 判断这个锁是不是自己的
- 如果是自己的锁,则重置时间
- 成功返回1,失败返回0
解锁






-- KEYS[1] = 锁对象唯一键
-- KEYS[2] = 该锁发布订阅channel
-- ARGV[1] = UNLOCK_MESSAGE
-- ARGV[2] = 锁过期时间
-- ARGV[3] = 加锁线程ID
-- 该线程未持有锁直接返回
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
if (counter > 0) then
-- 重置该线程锁过期时间
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
-- 重入次数等于0了
else
-- 释放锁
redis.call('del', KEYS[1]);
-- 向channel发布锁释放消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
解锁后最终取消自动续期的定时器:


加锁tryLock()





1-15 Redisson 公平锁Fair Lock

公平锁其实就是对所有人公平,大家去抢购商品去饭店吃饭,并不是一拥而入,而是有顺序的。那么上一节课的默认锁是非公平的,大家都可以抢,谁抢到了就谁获得锁。而公平锁则不是,我们不能让一开始来的人等太久啊,对吧,我们去吃饭,第一个排队的,那么必然第一个等候进去吃饭,而不是让后面来的先插队,这样多不公平啊。所以redisson也提供公平锁的机制让我们去进行实现,也就是当有很多线程同时申请锁的时候,这些线程都会进入先进先出的一个队列,只有前面的才会优先获得锁,其他线程只有等到前面的锁释放了,才会被分配锁,此时,这个锁的全称可以称之为:可重入的公平锁。
公平锁和分布式锁概念区别:公平锁也是分布式锁,但是分布式锁是为了保证业务的独占,处理公共资源的时候,不被其他请求(或者线程)影响,而公平锁是有序的顺序的去抢锁,和业务本身没有关系,不要搞混。
测试
由于apipost模拟的请求是在执行完毕后在发送的,再次我们可以通过打断点,在网页端进行模拟测试


测试结果取消,目前公平锁的顺序是无法保证的,最后一个访问的请求插队了:

使用公平锁后可以解决该问题。
1-16 Redisson 联锁MultiLock

当一个请求线程需要同时处理多个共享资源的时候,可以使用联锁,也就是一次性申请多个锁,同时锁住多个共享资源,这个联锁可以防止死锁的出现。
比如我们修改企业的时候,虽然现在只有一个共享资源,但是如果企业表水平分割了,分为了多个字表,那么字表其实也是共享资源,又或者说修改企业的同时还修改了其他共享资源,这个时候,其实都应该加锁的,就得使用联锁。如此,相关的共享资源都可以有原子性保障。
此外,联锁可以由不同的redisson实例来创建,如此,如果一个redisson实例节点宕机了,那么联锁就会失效。
联锁使用不多。
1-17 Redisson 红锁RedLock 以及其算法阐述

使用率不多,了解即可。
Redis分布式锁有一个致命的弱点,那就是redis服务器如果宕机了,那么锁肯定不能使用了,必定存在问题。
扩展主从哨兵:

如果使用主从哨兵,也有问题,setnx数据还没来得及同步给slave,这个时候master宕机,那么某个slave成为新的master,那么这个时候是没有锁的数据的,此时并发请求进来,将会再次获得锁(第二把新锁),那么此时就有问题了,从而导致锁机制失效。
集群形态,三主三从,其实也是同样的情况,也有可能造成丢失锁。
这个时候,我们需要使用红锁redlock的算法来进行处理,保证锁是OK不会失效。红锁是redis特有的专属算法,其他中间件不具备。

- 有5个redis节点,他们相互独立,都是独立运行在不同的服务器节点里的。
- 获得锁之前,先获得当前的时间戳,用于后面的计算。
- 从5个redis中去获得锁,每个节点都获得一下,使用setnx和之前一样,并且也需要设置锁的过期时间。如果获得锁时间太长则超时失败,因为这个节点可能宕机了,此时就跳过继续往下一个redis实例去尝试获得锁。
- 计算每个节点获得锁的时间,综合必须小于设置锁的时间,比如每个节点消耗了10秒,总计50秒,而我们的锁设置30秒,那肯定不行。
- 设置的节点数一般为单数,保证半数以上获得锁成功就表示当前获得分布式锁是OK的。
- 假设每个节点消耗1秒,那么初始超时的30秒,减去5秒,剩余25秒,那么此时会在25秒后释放锁,而不是30秒。
- 如果获得锁失败,则需要对所有节点的锁进行释放,因为我们不知道哪个节点成功哪个节点失败,所以统一对所有的节点进行解锁unlock操作。
- 如果业务操作成功,则对所有节点释放锁即可。
所以,如果面试的时候被问到,如果redis挂了,分布锁失效,你应该要回答红锁方面的相关内容,而不是说保证redis高可用,说集群高可用的意义不大。
1-18 Redisson 读写锁ReadWriteLock

二八原则,百分之20是写,百分之80是读, 并发的请求主要有并发读和并发写,那么读写锁呢,就是更加细化的控制,都是并发写请求,或者并发读写请求则不行,并发读呢是可以的。


lock(15,TimeUnit.seconds)15秒后锁失效,没有看门狗的api,读写锁key的名字要一样,否则会失效

测试:
- 写写:上一个请求写入完毕后,下一个请求才能执行写操作,和之前的锁机制一致
- 写读:读请求需要等待上一个写操作完毕后,才能读,避免读取到脏数据
- 读读:无所谓,可以并发
- 查看rdm,redis中写锁只能有1个,读锁可以存在多个

只有一行
有多行
1-19 Redisson 信号量Semaphore


Redisson的Semaphore本质上和JUC的意思一样,只是可以在分布式下更完善。
Semaphore信号量本质上也是用于限流的,比如红绿灯可以限制车流量,如果没有红绿灯,那么车流量一旦很大,那么基本上就会造成交通瘫痪。又比如节假日去迪士尼或者环球电影城,人流量很大,车位就会很紧缺,很多时候停车场的入口处都会有一个指示牌,显示剩余空余的停车位还有多少个,这个也是信号量。再比如,我没去吃饭,餐厅只能容纳10个人,那么后面的人就得排队,吃完一桌去进去一个人,这也是同样的道理。
基于JUC的信号量
基于JUC的信号量,可以测试锁住的资源,释放后,才会被后续


使用场景:可以用于限流,比如现在我有1万个并发,系统处理不过来,只能处理300个左右的请求,这个时候我们就只允许进来300个线程可以访问,超过的只能等待。
基于Redisson的信号量
上面的JUC信号量在分布式下会失效的,所以需要借助redisson来实现



测试:加锁和释放可以分开作为两个单独方法,手动控制,加锁过程可以观察redis。
依次测试加锁num=1,num=2,num=3,当num=4,num=5,num=6是处于阻塞状态,当释放num=1或num=2或num=3时num=4或num=5或num=6才可能获得锁



提升用户体现感可使用
![]()

注意:信号量一旦设置完成,会一直存在我们redis中,数值是不会更改的,对同一个key做信号量更改的话,需要删除再去设置
1-20 Redisson 闭锁CountDownLatch

闭锁:所有资源全部准备好,才算成功。也能称之为(倒计数)计数器。
场景:我们做危化需要发车,装货的时候不是装好货了, 才能走,我们有一个CountDownLatch,会有很多步骤,每个步骤必须做完并且检查完毕(所有的步骤数 ),才算好,这个时候我们有个信号灯,才会从红色变为绿色,否则车子是走不了的,因为我们运输的是危险化学品,并不是说走就走的,必须等待一切资源就绪,才能发车,要不然会相当危险。




当value等于0的时候,资源才可以释放




681

被折叠的 条评论
为什么被折叠?



