知识点:
单体锁存在的问题:
-
单体锁,即单体应用中的锁,通过加单体锁(synchronized或RentranLock)可以保证单个实例并发安全
-
单体锁是JVM层面的锁,只能保证单个实例上的并发访问安全
-
如果将单体应用部署到多个tomcat实例上,由负载均衡将请求分发到不同的实例
-
每个tomocat实例都是一个JVM进程,多实例下会存在数据一致性问题。
分布式锁:
-
分布式应用中所有线程都去获取同一把锁,但只有一个线程可以成功的获得锁,其他没有获得锁的线程必须全部等待,直到持有锁的线程释放锁。
-
分布式锁是可以跨越多个tomcat实例,多个JVM进程的锁,所以分布式锁都是设计在第三方组件中的
-
分布式锁都是通过第三方组件来实现的,目前主流的解决方案是使用Redis或Zookeeper来实现分布式锁
存在的问题:
出现用户超买,商家超卖的问题
具体案例:
添加相关依赖:
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置application.yml
# 添加redis数据库
spring:
redis:
port: 6379
database: 1
host: 127.0.0.1
编写具体实例:
@RestController
@RequiredArgsConstructor
public class LockController {
private final StringRedisTemplate redisTemplate;
@SneakyThrows
@GetMapping("/deductStock")
public String deductStock() {
System.out.println("用户正在下单……");
/**
* 单体锁
*/
int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (total > 0) {
total = total - 1;
Thread.sleep(3000);
redisTemplate.opsForValue().set("stock", String.valueOf(total));
System.out.println("下单成功!剩余库存为:" + total);
return "下单成功!剩余库存为:" + total;
}
System.out.println("用户下单失败!");
return "下单失败!剩余库存为:" + total;
}
}
测试(点击要快):
我们模拟了系统休眠 ,多线程同时进入一个方法体中,此时,票100同时卖给了两个用户!
解决方案:
单体锁:
使用synchronized关键字:修改方法或代码块,用于实现同步控制。当一个线程进入synchronized修饰的方法或代码块时,其他线程需要等待该线程执行完毕后才能进入。
其中,this关键字指的是,该类的具体实例,即LockController类的具体实例:
@RestController
@RequiredArgsConstructor
public class LockController {
private final StringRedisTemplate redisTemplate;
@SneakyThrows
@GetMapping("/deductStock")
public String deductStock() {
System.out.println("用户正在下单……");
/**
* 单体锁
*/
synchronized (this) {
int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (total > 0) {
total = total - 1;
Thread.sleep(3000);
redisTemplate.opsForValue().set("stock", String.valueOf(total));
System.out.println("下单成功!剩余库存为:" + total);
return "下单成功!剩余库存为:" + total;
}
System.out.println("用户下单失败!");
return "下单失败!剩余库存为:" + total;
}
}
}
测试结果:
虽然单体锁,解决了在同一个类中,多线程进入方法的问题,但是,当LockController并非单例,也会出现超卖现象:
存在问题:当项目部署到集群服务器中,由反向代理服务器,负载均衡。会导致出现多个LockController实例。
解决方法(使用分布式锁):
分布式锁:
首先创建一个工具类,用于注入静态的组件:
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext ac;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.ac = applicationContext;
}
public static <T> T getBean(Class<T> clazz){
return ac.getBean(clazz);
}
public static Object getBean(String name){
return ac.getBean(name);
}
}
定义一个工具类,用于获取锁,释放锁:
/**
* 分布式锁工具类
*/
public class LockUtil {
private static StringRedisTemplate redisTemplate = ApplicationContextHolder.getBean(StringRedisTemplate.class);
//获取锁的超时时间(自旋重试时间)
private static long waitTimeout = 10000L;
//锁的过期时间,防止死锁
private static long lockTimeout = 10L;
/**
* 获取分布式锁
*/
public static boolean getLock(String lockName, String value) {
//计算获取锁的超时时间
long endTime = System.currentTimeMillis() + waitTimeout;
//超时之前尝试获取锁
while (System.currentTimeMillis() < endTime) {
//判断是否能够获取锁,其实就是判断是否往redis中插入对应的key
Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, value, lockTimeout, TimeUnit.SECONDS);
if (flag) {
return true;
}
}
return false;
}
/**
* 释放分布式锁
*/
public static void unlock(String lockName, String value) {
if(value.equals(redisTemplate.opsForValue().get(lockName))){
redisTemplate.delete(lockName);
}
}
}
使用分布式锁进行加锁:
@RestController
@RequiredArgsConstructor
public class LockController {
private final StringRedisTemplate redisTemplate;
@SneakyThrows
@GetMapping("/deductStock")
public String deductStock() {
System.out.println(Thread.currentThread().getName() + "用户正在下单……");
/**
* 分布式锁
*/
String lockName = "stock_lock";
String value = UUID.randomUUID().toString();
if (!LockUtil.getLock(lockName, value)) {
return "获取锁失败……";
}
int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (total > 0) {
total = total - 1;
Thread.sleep(3000);
redisTemplate.opsForValue().set("stock", String.valueOf(total));
System.out.println("下单成功!剩余库存为:" + total);
LockUtil.unlock(lockName,value); //释放锁
return "下单成功!剩余库存为:" + total;
}
System.out.println("用户下单失败!");
LockUtil.unlock(lockName,value);
return "下单失败!剩余库存为:" + total;
}
}
测试结果:
存在问题:当用户进入后,拿到锁后,执行后续代码,但是锁到期了,锁被释放出来。后续的用户,也是可以进入线程当中的。依旧会出现抄买现象。
解决方法(第三方库来实现分布式锁 ):判断当前用户是否完成后续操作,如果没有完成就自动续签(加时长),直到用户完成后续操作。
Redisson:
Redisson是一个基于Redis的Java驻留对象框架,它提供了一套易于使用的API,用于操作Redis的数据结构和执行分布式操作。
Redisson是Redis官网推荐实现分布式锁的一个第三方类库,用起来更简单。
执行流程:
-
只要线程加锁成功(默认锁的超时时间为30s),Redisson就会启动一个用于监控锁的看门狗,它是一个守护线程,会每隔10秒检查一下,如果线程还持有锁,就会不断的延长锁的有效期(即每到20s就会自动续借成30s),也称为自动续期机制
-
当业务执行完,释放锁后,会关闭守护线程。
-
从而防止了线程业务还没执行完,而锁却过期的问题 。
首先引入相关依赖:
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.24.3</version>
</dependency>
编写代码,调用工具类:
@RestController
@RequiredArgsConstructor
public class LockController {
private final StringRedisTemplate redisTemplate;
private final RedissonClient redissonClient;
@SneakyThrows
@GetMapping("/deductStock")
public String deductStock() {
System.out.println(Thread.currentThread().getName() + "用户正在下单……");
/**
* 使用Redisson分布式锁
*/
String lockName = "stock_lock";
RLock rLock = redissonClient.getLock(lockName);
rLock.lock(); //获取锁
int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (total > 0) {
total = total - 1;
Thread.sleep(3000);
redisTemplate.opsForValue().set("stock", String.valueOf(total));
System.out.println("下单成功!剩余库存为:" + total);
rLock.unlock();
return "下单成功!剩余库存为:" + total;
}
System.out.println("用户下单失败!");
rLock.unlock();
return "下单失败!剩余库存为:" + total;
}
}
测试结果: