在多人访问网站时,如果不加锁,就会出现并发问题。下面我们先来测试进行模拟商品秒杀的场景:
首先我们编写两个方法一个用于下单减去库存,一个用于查询商品库存:
@Service
public class SecKillServiceImpl implements SecKillService {
/**
* 中秋活动 秒杀月饼 限量100000
*/
static Map<String, Integer> products;
static Map<String, Integer> stock;
static Map<String, String> orders;
static {
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
products.put("1001", 100000);
stock.put("1001", 100000);
}
private String queryMap(String productId) {
return "中秋活动,月饼特价,限量份"
+ products.get(productId)
+ " 还剩:" + stock.get(productId) + " 份"
+ " 该商品成功下单用户数目:"
+ orders.size() + " 人";
}
@Override
public String querySecKillProductInfo(String productId) {
return queryMap(productId);
}
@Override
public void orderProductMockDiffUser(String productId) {
int stockNum = stock.get(productId);
if (stockNum == 0) {
//库存不足
throw new SellException(100, "活动已经结束,请留意下次活动");
} else {
orders.put(KeyUtils.getUniqueKey(), productId);
stockNum = stockNum - 1;
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
stock.put(productId, stockNum);
}
}
}
编写Controller层进行测试
@RestController
@RequestMapping("/kill")
@Slf4j
public class SecKillController {
@Autowired
private SecKillService secKillService;
/**
* 查询秒杀活动特价商品的信息
* @param productId
* @return
*/
@GetMapping("/query/{productId}")
public String query(@PathVariable String productId)throws Exception{
return secKillService.querySecKillProductInfo(productId);
}
/**
* 秒杀,没有抢到获得"哎呦喂,xxxxx",抢到了会返回剩余的库存量
* @param productId
* @return
* @throws Exception
*/
@GetMapping("/order/{productId}")
public String skill(@PathVariable String productId)throws Exception{
log.info("@skill request, productId:" + productId);
secKillService.orderProductMockDiffUser(productId);
return secKillService.querySecKillProductInfo(productId);
}
}
一.我们先来测试以下不加锁的情况:
1.首先我们测试正常情况:下单三次,总量= 剩余+下单数量
2.使用压测工具apache ab进行模拟1000人下单的情况
输入命令:
ab -n 1000 -c 10 http://127.0.0.1:8080/kill/order/1001
接下来我们查询结果:
可以看到这时候的数量明显不对: 下单1003次,总量 < 剩余 + 下单数
3.这时我们使用传统的synchronized锁将整个方法锁起来继续测试:
//synchronized处理锁
@Override
public synchronized void orderProductMockDiffUser(String productId) {
int stockNum = stock.get(productId);
if (stockNum == 0) {
//库存不足
throw new SellException(100, "活动已经结束,请留意下次活动");
} else {
orders.put(KeyUtils.getUniqueKey(), productId);
stockNum = stockNum - 1;
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
stock.put(productId, stockNum);
}
}
这次我们同样模拟1000人下单:
可以明显看得到,请求速度非常得慢,虽所结果对的上,但系统将每个请求处理一遍,这样对服务器的性能要求就非常高。
4.接下来我们使用redis的分布式锁来进行测试
我们可以在进入下单的方法后将核心的方法加锁,然后离开后进行解锁
加锁
核心方法
解锁
我们使用redis的分布式锁来实现
首先引入maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
编写加锁和解锁的方法:
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁
* @param key
* @param value 当前事件+超时事件
* @return
*/
public boolean lock(String key,String value){
//加锁成功
if (redisTemplate.opsForValue().setIfAbsent(key,value)){
return true;
}
//假如currentValue=A先占用了锁 其他两个线程的value都是B,保证其中一个线程拿到锁
String currentValue = redisTemplate.opsForValue().get(key);
//锁过期 防止出现死锁
if (!StringUtils.isEmpty(currentValue) &&
Long.parseLong(currentValue) < System.currentTimeMillis()){
//获取上一步锁的时间
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
if (!StringUtils.isEmpty(oldValue) &&
oldValue.equals(currentValue)){
return true;
}
}
return false;
}
/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key,String value){
try {
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) &&
currentValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
log.error("【redis分布式锁】 解锁异常,{}",e);
}
}
}
我们修改原本的核心代码,对其进行加锁和解锁
@Service
public class SecKillServiceImpl implements SecKillService {
/** 超时时间 */
private static final int TIMOUT = 10000;
@Autowired
private RedisLock redisLock;
/**
* 中秋活动 秒杀月饼 限量100000
*/
static Map<String, Integer> products;
static Map<String, Integer> stock;
static Map<String, String> orders;
static {
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
products.put("1001", 100000);
stock.put("1001", 100000);
}
private String queryMap(String productId) {
return "中秋活动,月饼特价,限量份"
+ products.get(productId)
+ " 还剩:" + stock.get(productId) + " 份"
+ " 该商品成功下单用户数目:"
+ orders.size() + " 人";
}
@Override
public String querySecKillProductInfo(String productId) {
return queryMap(productId);
}
@Override
public void orderProductMockDiffUser(String productId) {
long time = System.currentTimeMillis() + TIMOUT;
//加锁
if (!redisLock.lock(productId,String.valueOf(time))){
throw new SellException(110,"没抢到,换个姿势再来一遍");
}
int stockNum = stock.get(productId);
if (stockNum == 0) {
//库存不足
throw new SellException(100, "活动已经结束,请留意下次活动");
} else {
orders.put(KeyUtils.getUniqueKey(), productId);
stockNum = stockNum - 1;
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
stock.put(productId, stockNum);
}
//解锁
redisLock.unlock(productId,String.valueOf(time));
}
}
接下来我们测试一下:同样1000人同时访问
可以看到只有33人访问了进来,这是由于很多人访问时并没有拿到锁,所以直接跳过了。这样就处理了多线程并发问题的同时也保证了服务器的性能的稳定。