目录
一 redission简介
1.1 redission介绍*
1.1.1 总体流程
Redisson分布式锁是一种基于redis实现的分布式锁,它利用redis的setnx命令实现分布式锁的互斥访问。同时还支持锁的自动续期功能,可以避免因为某个进程崩溃或者网络故障导致锁无法释放的情况。
只要线程一加锁成功,就会启动一个watchdog看门狗,它是一个后台线程,会每隔10秒检查一下,默认生存时间只有30秒,如果线程A还持有锁,那么就会不断的延长锁key的生存时间。可以使用reentracklock,公平锁,读写锁,信号量,闭锁等锁进行加锁操作,完成后然后释放锁。其他线程BCD判断加锁的次数为0,就可以进行加锁操作。
因此,Redisson就是使用watch dog解决了「锁过期释放,业务没执行完」的问题。
1.1.2 详解流程
1.加锁机制:
一个客户端A要加锁,它首先会根据hash算法选择一台机器,这里注意,仅仅只是选择一台机器。紧接着就会发送一段lua脚本到redis上,比如加锁的那个锁key为"mylock",并且设置的时间是30秒,30秒后,mylock锁就会被释放。
2.锁互斥机制:
如果这个时候客户端B来加锁,它也会会根据hash算法选择一台机器,然后执行了同样的一段lua脚本。它首先回来判断"exists mylock"这个锁存在吗?如果存在,则B会获得一个数字,这个数字就是mylock这个锁的剩余生存时间,此时客户端B就会进入到一个while循环,不停的尝试加锁
3.watch dog自动延期机制:
客户端A加锁的锁key,默认生存时间只有30秒,如果超过了30秒,客户端A还想一直持有这把锁,怎么办?其实只要客户端A一旦加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间
4.可重入加锁机制:
当客户端A获得mylock锁时,里面会有一个hash结构的数据:
mylock:{"8743c9c0-4907-0795-87fd-6c719a6b4586:1":1}
"8743c9c0-4907-0795-87fd-6c719a6b4586:1"就是代表客户端A,数字1就是代表这个客户端加锁了一次;如果客户端A还想要持有锁mylock,那么这个1就会变成2,依次累加。
5.释放锁机制:
如果发现加锁次数变为0了,那么说明这个客户端不再持有锁了,客户端B就可以加锁了。https://huaweicloud.youkuaiyun.com/637eeee1df016f70ae4c9eac.html
redisson最强大的地方就是提供了分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包,获得了协调分布式多线程并发系统的能力,降低了程序员在分布式环境下解决分布式问题的难度。
1.2 存在问题以及解决版本*
1.2.1 死锁问题
如果使用这个分布式锁的Redisson节点实例的客户端宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。
解决办法: 加上超时时间约束,超过阈值,自动释放。Redission可以通过加锁方法提供leaseTime的参数来指定加锁的时间,超过这个时间后锁自动解开。从而避免锁一直被占用而导致的死锁问题。
1.2.2 业务未结束,锁自动释放问题
解决办法:为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout
来另行指定。确保在业务未完成前,锁不会被其他线程获取。
watch-dog作用: 在获取锁成功后,给锁加一个watchdog,watch dog 会起一个定时任务,在锁没有释放且快要过期的时候进行续期。
1.2.3 互斥问题
在redis集群主从模式下,通过基于故障转移无法真正实现RedLock功能。
因为redis在进行主从复制时是异步完成的,比如在clientA获取锁后,主redis复制数据到从redis过程中崩溃了,导致没有复制到从redis中,然后从redis选举出一个升级为主redis,造成新的主redis中没有clientA上锁的信息,恰好这是clientB尝试获取到了锁,并且能够成功获取锁,导致互斥特性失效。
解决办法:Redission分布式锁支持MultiLock机制,可以将多个锁合并为一个大锁,对一个大锁进行统一的申请加锁以及释放锁。能够保证:
1.互斥,任何时候只能有一个client获取数据。
2.释放死锁:即使锁定资源的服务崩溃或者分区,仍然能释放锁。
3.容错性:只要多说redis节点(一半以上)在使用,client就可以获取和释放锁。
1.2.4 可重入和误删除
Redisson 在为每个锁关联一个线程 ID 和重入次数(递增计数器)作为分布锁 value 的一部分存储在 Redis 中,这样就避免了锁误删和不可重入的问题。
1.3 redission封装实现特性
1.3.1 redisson封装实现的特性
1.Redisson 可以设置分布式锁的过期时间,从而避免锁一直被占用而导致的死锁问题。
2.Redisson 在为每个锁关联一个线程 ID 和重入次数(递增计数器)作为分布锁 value 的一部分存储在 Redis 中,这样就避免了锁误删和不可重入的问题。
3.Redisson 还提供了自动续期的功能,通过定时任务(看门狗)定期延长锁的有效期,确保在业务未完成前,锁不会被其他线程获取。
如何使用Redisson实现分布式锁?-腾讯云开发者社区-腾讯云
二 redission常用子锁介绍
2.1 案例
2.1.1 工程结构
2.1.2 pom配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu</groupId>
<artifactId>distributed-lock</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>distributed-lock</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--SpringBoot与Redis整合依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- redisson 分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.5</version>
</plugin>
</plugins>
</build>
</project>
2.1.3 redis的启动与参数设置
2.2 重入锁
2.2.1 重入锁
基于Redis的Redisson分布式可重入锁RLock,
Java对象实现了java.util.concurrent.locks.Lock
接口。 Redisson还通过加锁的方法提供了leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
2.2.2 重入锁操作案例
1.代码截图
2.核心代码
@GetMapping("stock/rt")
public String decuceRt(){
RLock lock=this.redissonClient.getLock("my-rt");
lock.lock();
try {
//1.查询库存信息
String stock = redisTemplate.opsForValue().get("stock").toString();
// 2.判断库存是否充足
if (stock != null && stock.length() > 0) {
Integer st = Integer.valueOf(stock);
if (st > 0) {
//3.减库存操作
redisTemplate.opsForValue().set("stock", String.valueOf(--st));
}
}
}
finally {
lock.unlock();
}
return "ok!!!";
}
3.测试
4.查看redis
127.0.0.1:6379> get stock
"\"49\""
2.3 公平锁
2.3.1 公平锁
它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。接口API如下:
2.3.2 公平锁操作案例
1.核心代码
2.代码
@GetMapping("stock/fair")
public String decuceFairLock(){
RLock lock=this.redissonClient.getFairLock("myFairLock");
lock.lock();
try {
//1.查询库存信息
String stock = redisTemplate.opsForValue().get("stockfair").toString();
// 2.判断库存是否充足
if (stock != null && stock.length() > 0) {
Integer st = Integer.valueOf(stock);
if (st > 0) {
//3.减库存操作
redisTemplate.opsForValue().set("stockfair", String.valueOf(--st));
}
}
try {
TimeUnit.MILLISECONDS.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
finally {
lock.unlock();
}
return "ok123!!!";
}
3.设置redis
127.0.0.1:6379> get stockfair
"100"
4.访问
5.查看
127.0.0.1:6379> get stockfair
"\"99\""
127.0.0.1:6379>
2.4 联锁
2.4.1 联锁
基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock
对象关联为一个联锁,每个RLock
对象实例可以来自于不同的Redisson实例。要求每一个节点都得满足。则获取锁成功
2.5 redLock 红锁
2.5.1 redlock 红锁
基于Redis的Redisson红锁RedissonRedLock
对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock
对象关联为一个红锁,每个RLock
对象实例可以来自于不同的Redisson实例。大部分节点满足则获取锁成功
2.6 信号量
2.6.1 信号量
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore
采用了与java.util.concurrent.Semaphore
相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
2.6.2 信号量操作案例
1.代码
@GetMapping("stock/sp")
public String testsp(){
RSemaphore lock=this.redissonClient.getSemaphore("sp");
lock.trySetPermits(3);
try {
try {
lock.acquire();
this.redisTemplate.opsForList().rightPush("log","10086开始处理业务了"+Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(10+new Random().nextInt(10));
this.redisTemplate.opsForList().rightPush("log","10086处理完成业务逻辑,释放资源======"+Thread.currentThread().getName());
lock.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
finally {
}
return "信号量!!!";
}
2.测试
3.查看redis
2.7 闭锁
2.7.1 原理
基于 Redisson 的 Redisson 分布式闭锁( CountDownLatch ) Java 对象 RCountDownLatch 采用了与java.util.concurrent.CountDownLatch 相似的接口和用法。
2.7.2 操作案例
1.代码截图
2.核心代码
@GetMapping("stock/suo")
public String testsuo(){
RCountDownLatch cd1=this.redissonClient.getCountDownLatch("cd1");
cd1.trySetCount(3);
System.out.println("进入 lock 之中喽。。。。。。");
try {
cd1.await();
}
catch (Exception e){
e.printStackTrace();
}
return "cd lock!!!";
}
@GetMapping("stock/sf")
public String testsf(){
RCountDownLatch cd1=this.redissonClient.getCountDownLatch("cd1");
try {
cd1.countDown();
System.out.println("执行 countdown---。。。。。");
}
catch (Exception e){
e.printStackTrace();
}
return "cd unlock!!!";
}
3.测试
这个接口请求3次,countdown变为0
这个接口才能请求完成。
2.8 读写锁
2.8.1 读写锁
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
2.特点:
同时访问写:一个写完,等待一会,另外一个写开始。
同时访问读:不用等待
先写后读:读要等待写完成后执行。
先读后写:写要等待读完成后执行。
三 Redission源码分析
3.1 关于 getLock("xxx")的作用
1.在getLock方法中设定锁的资源名称rLock,进行初始化锁名称
2.间接在RedissonLock构造函数资源名称赋值给成员变量entryName;同时父类构造函数super(xxx,name);
3.查看父类RedissonExpirable 的构造函数,调用其父类的super函数
4.查看:在RedissonObject类中,通过构造函数赋值给成员变量 name
3.2 加锁操作
1.加锁操作
总流程:通过exits判断,如果锁不存在,则设置值和过期时间,加锁成功。
通过hexits判断,如果锁已经存在,摈弃锁的是当前线程,则证明是重入锁,加锁成功;
如果锁已存在,但锁的不是当前线程,则证明其它线程持有锁,返回当前锁的过期时间(代表了锁key的剩余生存时间),加锁失败。
2.加锁操作使用lua脚本保证原子性
如果锁不存在,则通过hset设置它的值,并设置过期时间。
如果锁存在,并且锁的是当前线程,则通过hincryby给它递增加1;
如果锁存在,并且不是当前线程的,则返回剩余过期时间ttl。
3.3 续锁功能
1.锁续期的方法
2.锁续期逻辑:
这里在初始化RedissionLock的构造函数时,初始化InternalLockLeaseTime的值,默认为30s,也就是每隔10s进行续锁一次。可以理解为delay的值为InternalLockLeaseTime/3;
3.4 解锁功能
1.清除:CHM中缓存数据
2.解锁逻辑
四 实操案例
4.1 案例操作
4.1.1 pom依赖配置
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.1</version>
</dependency>
4.1.2 redisson配置文件
package com.atguigu.distributed.lock.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @ClassName: RedissonConfig
* @Description: TODO
* @Author: admin
* @Date: 2024/01/14 23:18:50
* @Version: V1.0
**/
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
// 可以用"rediss://"来启用SSL连接
// config.useSingleServer().setAddress("redis://192.168.43.4:6379");
config.useSingleServer().setAddress("redis://192.168.43.4:6379").setPassword("123456");
return Redisson.create(config);
}
}
4.1.3 核心加锁代码
@Autowired
private RedissonClient redissonClient;
public void checkAndLockByRedisson() {
// 加锁,获取锁失败重试
RLock lock = this.redissonClient.getLock("rlock");
lock.lock();
System.out.println("redisson的锁....");
// 先查询库存是否充足
Stock stock = this.stockMapper.selectById(1L);
// 再减库存
if (stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount() - 1);
this.stockMapper.updateById(stock);
}
// 释放锁
lock.unlock();
}
4.1.4 controller调用
4.1.5 验证调试
1.启动服务
2.启动redis
[root@localhost export]# redis-server /myredis/redis.conf
[root@localhost export]# redis-cli -a 123456 -p 6379
3.启动zk
[root@localhost export]# cd apache-zookeeper-3.7.0-bin/
[root@localhost apache-zookeeper-3.7.0-bin]# cd bin
[root@localhost bin]# ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /root/export/apache-zookeeper-3.7.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
4.启动nginx
5.启动jemter
6.查看结果
2.查看结果
7.查看db结果
测试前
测试后