相信很多同学都听说过分布式锁,但也仅仅停留在概念的理解上,这篇文章会从分布式锁的应用场景讲起,从实现的角度上深度剖析redis如何实现分布式锁。
一、超卖问题
我们先来看超卖的概念:
当宝贝库存接近0时,如果多个买家同时付款购买此宝贝,或者店铺后台在架数量大于仓库实际数量,将会出现超卖现象。超卖现象本质上就是买到了比仓库中数量更多的宝贝。
本文主要解决超卖问题的第一种,同时多人购买宝贝时,造成超卖。
测试代码
那么超卖问题是如何产生的呢?我们准备一段代码进行测试:
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 第一种实现,进程内就存在线程安全问题
* 可以只启动一个进程测试
*/
@RequestMapping("/deduct_stock1")
public void deductStock1(){
String stock = stringRedisTemplate.opsForValue().get("stock");
int stockNum = Integer.parseInt(stock);
if(stockNum > 0){
//设置库存减1
int realStock = stockNum - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.println("设置库存" + realStock);
}else{
System.out.println("库存不足");
}
}
这段代码中,使用redis先获取库存数量(当然实际场景中不会只保存一个全局库存数,应该根据每一个商品单元(sku)保存一份库存数)。
String stock = stringRedisTemplate.opsForValue().get("stock");
int stockNum = Integer.parseInt(stock);
接下来,判断库存数是否大于0:
-
如果大于0,将库存数减一,通过set命令,写回redis
这里没有使用redis的decrement命令,因为此命令在redis单线程模型下是线程安全的,而为了可以模拟线程不安全的情况将其拆成三步操作
//设置库存减1
int realStock = stockNum - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.println("设置库存" + realStock);
- 如果小于等于0,提示库存不足
JMeter测试
通过JMeter进行并发测试,看下会不会出现超卖的问题:
1.启动tomcat
这种情况下,只需要启动一个tomcat就会出现超卖。我们先启动一个tomcat在8080端口上。
2.下载JMeter
Apache JMeter是Apache组织开发的基于Java的压力测试工具。
从官网上下载即可:
https://jmeter.apache.org/download_jmeter.cgi
下载完之后解压,运行bin目录下的jmeter.bat,显示如下界面:
如果嫌字体太小,可以选择放大:
3.配置JMeter
在Test Plan上点击右键,创建线程组(Thread Group)
配置一下具体参数:
Number of Threads
同时并发线程数Ramp-Up Period(in-seconds)
代表隔多长时间执行,0代表同时并发。假设线程数为100, 估计的点击率为每秒10次, 那么估计的理想ramp-up period 就是 100/10 = 10 秒Loop Count
循环次数
这里给出500是为了直接测试并发500抢,看看能不能正好把500个货物抢完。
添加Http请求:
添加请求URL:
添加聚合结果,用来显示整体的运行情况:
到此为止JMeter的配置结束。
4.设置库存量
启动redis-server,使用redis-client连接:
把库存数设置为500。
5.开始测试
点击运行按钮,启动测试:
首先我们看到聚合报告里输出的结果:
错误率0%,样本数500,证明500个请求都已经执行,但是发现控制台输出如下:
很显然,一份商品都被卖了多次,这显然是不合理的。
原因分析
现在我们只启动了一个tomcat,在单jvm进程的情况下,tomcat会使用线程池接收请求:
而由于每个线程可能同时获取到库存量,所以库存量在两个线程中显示的都是500,然后两个线程就继续进行扣减库存操作,得出499写回redis中,在这个过程中,显然存在线程安全的问题。同一个商品被卖出了2份,超卖问题就出现了。
二、加锁优化
synchronized锁
要保证单jvm中线程安全,最简单直接的方式就是添加synchronized关键字,那么这样行不行呢,我们来做一个测试:
/**
* 第二种实现,使用synchronized加锁
* 可以只启动一个进程测试
*/
@RequestMapping("/deduct_stock2")
public void deductStock2(){
synchronized (this){
String stock = stringRedisTemplate.opsForValue().get("stock");
int stockNum = Integer.parseInt(stock);
if(stockNum > 0){
//设置库存减1
int realStock = stockNum - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.println("设置库存" + realStock);
}else{
System.out.println("库存不足");
}
}
}
在进行扣减库存前,先通过synchronized关键字,对资源加锁,这样就只有一个线程能进入到扣减库存的代码块中。来测试一下:
重置库存