知识点:
单体锁存在的问题:
-
单体锁,即单体应用中的锁,通过加单体锁(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: 6379database: 1host: 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;@Overridepublic 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中插入对应的keyBoolean 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;}
}
测试结果: