本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、场景模拟
在抢红包或秒杀商品的时候,肯定会有高并发的情况出现,程序中如果出现库存重复减扣的情况,那肯定是不行的!接下来模拟一下高并发下的库存重复减扣问题以及相应的解决方案。
1. 在测试前,需要预先给redis设置一个key用来作为库存
2. java代码如下:
@GetMapping("/redis/stuck")
public String getRedisStuck() {
//在高并发场景下会出现什么问题?
int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
String str="";
if (stock>0){
int realStock=stock-1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
str="扣减成功,剩余库存:"+realStock;
System.out.println(str);
}else{
str="没有库存啦!";
System.out.println(str);
}
return "end";
}
复制代码
3 . 使用nginx负载2个tomcat的架构,后面访问的时候在nginx里配了域名bingbing.com,注意在windows的hosts文件添加域名和ip的映射 192.168.254.10 bingbing.com
nginx的配置参考: nginx负载均衡多个tomcat实例_Dream_it_possible!的博客-CSDN博客_nginx负载均衡多个tomcat
单线程下不会出现问题,但在高并发的情况下会出现什么问题呢?接下来使用压测工具Jmeter模拟高并发场景下,会出现的问题。
Jmeter配置
1. 添加线程组,参数说明: 20个并发数,0表示立即执行,5表示循环5次。
2. 添加接口:
3. 启动Jmeter线程组,两台机器的打印结果如下:
- 可以发现,在并发量比较大的时候,出现库存没有扣除的情况!
二、解决方案
1. 加锁
使用setIfAsent()方法给key加锁,拿到锁了的线程才能执行。
@GetMapping("/redis/stuck")
public String getRedisStuck() throws IOException {
String lockKey="product001";
try{
Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"华为手机");
if (!result){
return "请稍后重试!";
}
int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
String str="";
if (stock>0){
//思考,如果执行在这时发生宕机时,怎么办?,finally又执行不了,key没有删除,其他线程不能拿到锁,产生死锁。
int realStock=stock-1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
str="扣减成功,剩余库存:"+realStock;
System.out.println(str);
}else{
str="没有库存啦!";
System.out.println(str);
}
}finally {
//解锁
stringRedisTemplate.delete(lockKey);
}
return "end";
}
复制代码
存在的问题: 如果在if的时候出现程序宕机,拿到锁的key将会永远不会释放,出现死锁问题!
2. 设置失效时间
给锁添加失效时间,这样就不会出现用永远不会释放的问题。使用IfAbsent()方法同样可以设置失效时间!即拿到锁的线程可以继续执行,没拿到锁的线程先返回。修改如下代码:
Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"华为手机",10 ,TimeUnit.SECONDS);
if (!result){
return "请稍后重试!";
}
复制代码
完整代码,如下:
@GetMapping("/redis/stuck")
public String getRedisStuck() throws IOException {
String lockKey="product001";
try{
//设置失效时间
Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"华为手机",10 ,TimeUnit.SECONDS);
//在高并发场景下会出现什么问题
if (!result){
return "请稍后重试!";
}
int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
String str="";
if (stock>0){
//思考,如果执行在这时发生宕机时,怎么办?,finally又执行不了,key没有删除,其他线程不能拿到锁,产生死锁。
int realStock=stock-1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
str="扣减成功,剩余库存:"+realStock;
System.out.println(str);
}else{
str="没有库存啦!";
System.out.println(str);
}
}finally {
//解锁
stringRedisTemplate.delete(lockKey);
}
return "end";
}
复制代码
上述改动用redis锁失效的方法虽然在一定程度上解决了死锁问题, 但在高并发场景下仍然存在锁永久失效的问题!
3. 使用UUID
分析: 当有线程进来的时候,给每一个线程生成一个UUID,然后将UUID存放到lockey的value里,释放锁前判断从redis拿到的锁对应的UUID是否与当前生成的UUID相等,如果相等,则释放锁!
@GetMapping("/redis/stuck")
public String getRedisStuck() throws IOException {
String lockKey="product001";
String clientId= UUID.randomUUID().toString();
try{
//设置失效时间
Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10 ,TimeUnit.SECONDS);
//在高并发场景下会出现什么问题
if (!result){
return "请稍后重试!";
}
int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
String str="";
if (stock>0){
//思考,如果执行在这时发生宕机时,怎么办?,finally又执行不了,key没有删除,其他线程不能拿到锁,产生死锁。
int realStock=stock-1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
str="扣减成功,剩余库存:"+realStock;
System.out.println(str);
}else{
str="没有库存啦!";
System.out.println(str);
}
}finally {
//解锁
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
复制代码
此方法解决了线程释放其他线程的锁的可能!存在的问题: 程序的性能会比较低。
4. 强行给锁续命
思路: 有线程过来的时候,给当前线程开启一个分线程,分线程添加一个定时器,每隔锁失效时间的1/3时长来定时执行,然后判断key是否存在锁,如果存在锁那么给该锁的失效时间延长。
5 .redisson 分布式锁
redisson分布式锁能够保证一个集群下只有一个线程能够拿到锁,而且性能高,分布式架构中首选使用此方法加锁。
添加依赖:
<!--redisson分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
复制代码
启动的时候初始化,在主方法的类里,添加一个@Bean,注意: 如果redis有密码的话,需要设置密码。
@Bean
public Redisson redisson(){
//初始化redisson客户端,单机模式
Config config=new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0).setPassword("123456");
return (Redisson)Redisson.create(config);
}
复制代码
使用的时候直接@autowire注解即可!
@Autowired
private Redisson redisson;
复制代码
在执行减库存前使用 lock()方法,执行lock方法的线程才能够继续往后执行,没有获取到lock的线程会进入阻塞状态。
@GetMapping("/redis/stuck")
public String getRedisStuck() throws IOException {
String lockKey="product001";
RLock lock= redisson.getLock(lockKey);
try{
lock.lock(10,TimeUnit.SECONDS);
int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
String str="";
if (stock>0){
//思考,如果执行在这时发生宕机时,怎么办?,finally又执行不了,key没有删除,其他线程不能拿到锁,产生死锁。
int realStock=stock-1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
str="扣减成功,剩余库存:"+realStock;
System.out.println(str);
}else{
str="没有库存啦!";
System.out.println(str);
}
}finally {
//解锁
lock.unlock();
}
return "end";
}
复制代码
redisson核心有一个watch dog机制,时存在锁的key 给延时失效时间,防止当前线程没有用完该key时,被其他线程抢去了该key的锁,这样可能会导致其他的业务问题。