1.项目中分布式锁我们使用的基于reids的框架 : Redisson框架
2.底层原理是使用Lua脚本保证原子性的
(1)加锁:
正常情况下我们会调用tryLock进行加锁,第一个参数锁的过期时间,第二个参数是租约时间为了保证核心代码成功执行,第三个是时间单位,在进行加锁的时候会触发watchdog(看门狗)机制,当核心业务执行不完的时候锁就过期了,watchdog会对于锁的持有时间(10秒钟)进行自动续约,加锁的lua脚本是这样实现的:
- 判断redis中是否有锁,因为==0代表着没有锁,说明自己可以抢锁,那么我们就让锁的数量+1 并设置锁的过期时间
- 判断是否是自己线程的锁,因为==1,代表着是当前线程的可重入锁,那么直接让可重入锁的数量+1,并且重置锁的过期时间
- 如果前两个判断都没走说明抢锁失败,直接返回锁剩余的时间即可
这个时候其他线程处于阻塞状态,会订阅解锁的信号量,只要解锁的时候订阅到这个信号量直接通知阻塞的线程自旋抢锁即可,这样做可以避免其他线程上来就自旋浪费系统资源
(2)解锁:
- 先判断是否是自己的锁,如果不是,不做任何操作,直接返回null
- 如果是自己的锁,让锁的可重入次数-1
- 判断可重入锁的数量是否>0,如果是大于0的话,说明业务没有执行完毕,直接对锁续时
- 如果锁<=0的,说明所有的可重入锁都已经解除成功,删除当前线程持有的锁,并释放一个解锁成功的信号量,这个时候加锁的线程就可以订阅到这个信号量进行自旋抢锁了.
Redisson的pom.xml的依赖我们使用的是:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.12.5</version>
</dependency>
项目中完整pom.xml依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.45</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.12.5</version>
</dependency>
</dependencies>
写之前要先简单了解Redisson原理,就要知道Lua脚本:
1.这两个一个参数是自己定义的过期时间是多少,一个定义的时间单位,点击tryLock,进入查看service
1
2.这是接口层通过接口去查看他的实现层,选择redisson的,我们要查看的也是他
3.调用了自身重载的三个方法,没有租约时间的情况下他默人设置是租约时间-1,不管怎么样设置都会走这三方法,然后继续点tryLock.
4.核心就是尝试获取到锁,点击tryAcquire继续往下看
5,这是一个嵌套的方法,首先要先看里面的方法,因为里面的方法会返回给我们一个结果集给我们,点击tryAcquireAsync继续往下看
6.尝试获取锁的时候就会触发看门狗这个机制,继续点击Watchsog进去
7.我们看到看门狗过期时间,继续点进去
8.我们可以看到这个设置的时间是30*1000;如果是指定三个参数的情况下走的是自己的租约时间,不然就是走这个默认的续约30*1000这个时间.
9.倒回到第6看门狗哪里,tryLockInnerAsync尝试获取锁,点进去继续看.
10.Redisson如何实现加锁和解锁的就是方框这里的Lua脚本实现的,lua脚本代码可以根据上面加锁和解锁理解.
其次要了解Synchronized锁升级的过程?
偏向锁:当有一个线程访问共享资源的时候 这个线程就是偏向锁或者叫做无锁状态
轻量级锁:当有少量线程都需要争抢共享资源的使用权的时候那么就有偏向锁升级为轻量级锁,一个线程得到锁之后,其余线程处于自旋抢锁,最终还是会把机会大概率给之前偏向锁的线程
重量级锁:当有大量线程需要使用共享资源的时候,一个线程已经得获取到锁了,我们不能让其余大量线程自旋,这样太浪费系统资源了,会给cpu下达一个阻塞所有线程的指令,当之前的线程释放锁之后,再通知这些阻塞线程自旋抢锁
11.方框单词的意思是订阅的意思,针对于访问量大时间短的情况下,如果线程数特别多还使用自旋的话就会形成死循环消耗系统资源,所以针对应该使用IO阻塞,使用订阅就是为了考虑什么时间线程去进行自旋抢锁,抢到锁就会进行上锁,锁执行完毕就会解锁,并就会发送一个信号量
12.接收到信号量线程就会进行自旋抢锁,这样中间等待就不会浪费系统资源了
分布式锁的买票场景的使用:
application.yml配置:
server:
port: 8080
spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
database: 1
创建一个RedissonClientConfig类:
@Configuration
public class RedissonClientConfig {
@Bean
public RedissonClient redissonClient(){
//怎么把redis的地址告诉给redisson客户端?
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
config.useSingleServer().setPassword("123456");
config.useSingleServer().setDatabase(1);
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
在conrotion层创建一个TicketController类:
@RestController
@RequestMapping("/thread")
public class TicketController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisLock redisLock;
@Autowired
private RedissonClient redissonClient;
//买票
//线程安全问题:
//当多个线程同时对一个共享资源有写操作的时候,导致最终和实际不符
//负票:代码逻辑太长 导致多个线程同时进入程序内部
//重复票:num--和--num不是一个原子性的操作 所以产生了重票
//方法区:
//非静态方法锁定的是this(当前对象)
//静态方法锁定的是类.class文件
ReentrantLock lock = new ReentrantLock();
@GetMapping("/sub")
public void sub() {
//先获取到这个锁
RLock flag = redissonClient.getLock("flag");
try {
boolean b = lock.tryLock(2, TimeUnit.SECONDS);
if (b) {
String num = stringRedisTemplate.boundValueOps("num").get();
if (Integer.valueOf(num) > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// num--;
Long count = stringRedisTemplate.boundValueOps("num").decrement();
System.out.println(Thread.currentThread().getName() + "剩余票量为:" + count);
} else {
System.out.println("没票了");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
在lock层创建RedisLock类:
@Component
public class RedisLock {
@Autowired
private RedisTemplate redisTemplate;
//使用redis手写分布式锁可能碰到的问题
//1.一个线程已经抢到锁了 其余线程应该处于阻塞等待状态
//2.需要设置一个锁超时时间 避免死锁问题
//3.一个核心业务的执行时间如果超过你设置的锁超时时间 就会导致正常的业务执行出现问题
//redis推出一款框架:分布式锁解决框架redisson
//加锁
public boolean lock(String key){
//判断redis中是否存在key的数据 如果存在返回false 如果不存在向redis中添加数据 返回true
//sexnx 是解决分布式锁的重要命令 在Java代码中体现的就是setIfAbsent
return redisTemplate.boundValueOps(key).setIfAbsent("ok");
}
//解锁
public boolean unlock(String key){
String name = Thread.currentThread().getName();
//获取redis的线程名称
Object tName = redisTemplate.boundValueOps(key).get();
//判断
if (Objects.equals(name,tName)){
redisTemplate.delete(key);
return true;
}
return false;
}
}