分布式锁
目录
1. 模拟高并发场景秒杀下单
1.1 导入依赖
1.2 配置application.yml文件
1.3 场景模拟
1.4 案例演示
2. JVM级锁与redis级分布式锁
2.1 JVM级锁
3. redis级分布式锁
3.1 什么是setnx
3.2 场景分析
4. redisson分布式锁
4.1 什么是Redisson
4.2 Redisson工作原理
4.3 入门案例
1. 模拟高并发场景秒杀下单
1.1 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--commons-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.0</version>
</dependency>
1.2 配置application.yml文件
server:
port: 8081
spring:
redis:
host: 127.0.0.1
password: 123456
database: 0
port: 6379
1.3 场景模拟
@RestController
public class RedissonController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/updateStock")
public String updateStock() {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
return "end";
}
}
1.4 案例演示
- 示例一:单线程情况
直接打开浏览器输入:http://localhost:8081/updateStock,查看redis中库存扣减情况。
- 示例二:多线程情况
第1步:配置多启动服务
第2步:配置nginx,实现负载均衡
upstream tomcats{
server 127.0.0.1:8081 weight=1;
server 127.0.0.1:8082 weight=2;
}
server
{
listen 80;
server_name localhost;
location / {
proxy_pass http://tomcats/;
}
}
第3步:配置jmeter,实现压测
创建测试用例,循环发送4组线程,每组200个;
查看redis中库存结果为0;查看多服务控制台信息均显示扣减失败,库存不足提示。
- 结果分析
1)在单线程情况下,调用updatestock方法扣减库存,订单下单正常(没啥好说的)
2)在多线程情况下,调用updatestock方法扣减库存正常,订单下单异常(超卖了)原因分析:在高并发情况下同时多个线程调用updateStock方法,按照正常思路线程1、线程2、线程3应该是分别实现库存减一(在库存为100的情况下,现在应该剩余97),同时生成三个秒杀订单;然后并发情况下根本不会按照剧本设计来执行,而是出现了线程1、线程2、线程3同时扣减库存,导致库存剩余99,但是订单却产生了3个,说明超卖了。
2. JVM级锁与redis级分布式锁
2.1 JVM级锁
@RestController
public class RedissonController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/updateStock")
public String updateStock() {
//jvm级锁,单机锁
synchronized (this){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}
return "end";
}
}
jvm级的同步锁,单机锁。上述同步代码块中,在单机环境下同一时刻只有一个线程能进行秒杀下单库存扣减,完毕之后才能有后续线程进入。但是在分布式环境下依然还是会出现商品超卖情况。
重新启动jmeter压测,连续发送4组,每组200个请求。
3. redis级分布式锁
3.1 什么是setnx
格式:setnx key value
将key的值设置为value,当且仅当key不存在;若给定的key不存在,则setnx不做任何动作。 setnx是set if not exists
(如果不存在,则set)的简写。
setnx "zking" "xiaoliu" 第一次设置有效
setnx "zking" "xiaoliu666" 第二次设置无效
第一次使用setnx设置zking直接成功,第二次使用setnx设置zking则失败,也意味着加锁失败。
- redis级分布式锁之setnx使用
@RestController
public class RedissonController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/updateStock")
public String updateStock() {
//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking");
if(!flag)
return "error_code";
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
//解锁
stringRedisTemplate.delete(lockKey);
}
return "end";
}
3.2 场景分析
基于以上redis分布式锁setnx的代码,实现场景分析。
- 问题1:执行扣减库存业务时出现异常,导致无法正常删除锁,从而形成死锁。
解决办法:通过try/catch/finally代码块解决。
//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
try{
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking");
if(!flag)
return "error_code";
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}finally{
//解锁
stringRedisTemplate.delete(lockKey);
}
- 问题2:执行扣减库存业务是如果Redis服务宕机,基于上述问题1的finally块就无意义了,还是死锁。
解决办法:加锁时设置过期时间,确保原子性。
//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
try{
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking",10,TimeUnit.SECONDS);
if(!flag)
return "error_code";
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}finally{
//解锁
stringRedisTemplate.delete(lockKey);
}
- 问题3:高并发场景下,线程执行先后顺序无法把控(自己加的锁被其他线程释放掉了,o(╥﹏╥)o)
场景分析:
线程1:业务执行时间15s,加锁时间10s,那么导致业务未执行完成锁被提前释放;
线程2:业务执行时间8s,加锁时间10s;
线程3:业务执行时间5s,加锁时间10s,那么导致线程2的任务还没有执行完成就是线程3将所删除掉了;
以此类推,只要是高并发场景一直存在,那么锁一直处于失效状态(永久失效)
解决办法:可以在加锁的时候设置一个线程ID,只有是相同的线程ID才能进行解锁操作。
//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
String clientId= UUID.randomUUID().toString();
try{
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10,TimeUnit.SECONDS);
if(!flag)
return "error_code";
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}finally{
//只有是相同的线程ID时才进行解锁操作
if(stringRedisTemplate.opsForValue().get(lockKey).equals(clientId)) {
//业务代码执行完毕删除redis锁(解锁)
stringRedisTemplate.delete(lockKey);
}
}
问题4:锁要加多次时间才是最合理有效的?
解决办法:redisson,看门狗机制。
4. redisson分布式锁
4.1 什么是Redisson
Redisson - 是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象,Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 更侧重对 Redis 数据库的 CRUD(增删改查),而 Redisson API 侧重于分布式开发。
特点:
-
互斥:在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。
-
防止死锁:在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。
-
性能:对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。所以在锁的设计时,需要考虑两点。
-
锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。
-
锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。
-
-
重入:ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。
4.2 Redisson工作原理
4.3 入门案例
- 创建RedissonConfig配置类
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
@Bean
public RedissonClient redissonClient(){
Config config=new Config();
String url="redis://"+host+":"+port;
config.useSingleServer().setAddress(url).setPassword(password).setDatabase(0);
return Redisson.create(config);
}
}
- 使用redisson分布式锁实现秒杀下单
@RequestMapping("/updateStock")
public String updateStock() {
String lockKey="lockKey";
RLock clientLock = redissonClient.getLock(lockKey);
clientLock.lock();
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
//解锁
clientLock.unlock();
}
return "end";
}
重新启动jmeter压测,连续发送4组,每组200个请求。查看多服务控制台,结果显示秒杀订单下单正常,无超卖情况发生。