在单机应用中,防止超卖可以使用jdk自带的
synchronized
关键字来处理。但是在分布式系统应用下使用synchronized
关键字就不生效了。那我们可以怎么做呢?下面我用两种解决方案实现,下面实现代码。
synchronized
来试一下,看看是否有效?@Slf4j
@RestController
@RequestMapping("miaosha")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MiaoShaController {
private final RedisTemplate<String, String> redisTemplate;
private final Object obj1 = new Object();
private final Object obj2 = new Object();
@PostMapping("distributedLock1")
public void distributedLock1() {
synchronized (obj1) {
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int remainingStock = stock - 1;
redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
log.info("1扣减成功!剩余库存:" + remainingStock);
} else {
log.error("1库存不足!");
}
}
}
@PostMapping("distributedLock2")
public void distributedLock2() {
synchronized (obj2) {
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int remainingStock = stock - 1;
redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
log.info("2扣减成功!剩余库存:" + remainingStock);
} else {
log.error("2库存不足!");
}
}
}
}
注:这里我使用了两个接口来模仿分布式环境,使用压力测试工具运行。这里我使用的是jmeter
,本篇文章就不说怎么安装使用了。
200个并发,发起两次,两个接口,一共800次请求。
点击运行,查看结果。
可以看到出现了超卖的情况,由于在分布式环境下synchronized
只作用于同一个jvm下。
redis
中的setIfAbsent
(相当于jedis
中的setnx
)将 key 的值设为 value,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是SET if Not eXists的简写。
改造后的代码一
@Slf4j
@RestController
@RequestMapping("miaosha")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MiaoShaController {
private final RedisTemplate<String, String> redisTemplate;
@PostMapping("distributedLock1")
public void distributedLock1() {
// 改造成redis的setnx
String lockKey = "lockKey";
// 添加uuid的value,防止程序运行10秒以上之后锁被自动清除,执行finally的时候会把后面进来的请求加好的锁删除,会造成超卖现象
String clientId = String.valueOf(UUID.randomUUID());
Boolean bool = false;
try {
// 设置redis锁,10秒后自动清除
bool = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
if (!bool) {
log.warn("1业务繁忙,请稍后再试!");
return;
}
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int remainingStock = stock - 1;
redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
log.info("1扣减成功!剩余库存:" + remainingStock);
} else {
log.error("1库存不足!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 为了保险起见,这边再把锁删除
if (bool && clientId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
@PostMapping("distributedLock2")
public void distributedLock2() {
// 改造成redis的setnx
String lockKey = "lockKey";
// 添加uuid的value,防止程序运行10秒以上之后锁被自动清除,执行finally的时候会把后面进来的请求加好的锁删除,会造成超卖现象
String clientId = String.valueOf(UUID.randomUUID());
Boolean bool = false;
try {
// 设置redis锁,10秒后自动清除
bool = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
if (!bool) {
log.warn("2业务繁忙,请稍后再试!");
return;
}
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int remainingStock = stock - 1;
redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
log.info("2扣减成功!剩余库存:" + remainingStock);
} else {
log.error("2库存不足!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 为了保险起见,这边再把锁删除
if (bool && clientId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
}
还是800次请求,测试运行结果:
可以看到库存没有出现了超卖,800的请求有24个人抢到了,100个库存,现在看看redis中是否剩余76。
这样实现基本在一般业务下是没有问题的,但是在超级高并发的情况下,也有可能造成超卖的情况。虽然概率很小,但是我们也要优化到最优。
redisson
实现分布式锁官网:https://redisson.org
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
以下是Redisson的结构:
Redisson作为独立节点 可以用于独立执行其他节点发布到分布式执行服务 和 分布式调度任务服务 里的远程任务。
redisson.getLock底层实现原理
会生成一个长达15秒的锁,在业务代码执行过程中,redisson会判断当前业务是否执行完毕,若某种原因造成业务无法15秒内执行完,redisson会自动延长15秒的时间。我们只需要最后释放锁就可以了,操作简单。
改造后的代码二
package xgg.miaosha.demo.controller;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author xiegege
* @date 2020/10/6 14:17
*/
@Slf4j
@RestController
@RequestMapping("miaosha")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MiaoShaController {
private final RedisTemplate<String, String> redisTemplate;
private final Redisson redisson;
@ApiOperation("秒杀、并发场景下分布式锁")
@PostMapping("distributedLock1")
public void distributedLock1() {
// 改造成redisson
String lockKey = "lockKey";
// 获取锁对象
RLock lock = redisson.getLock(lockKey);
try {
// 加锁
lock.lock();
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int remainingStock = stock - 1;
redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
log.info("1扣减成功!剩余库存:" + remainingStock);
} else {
log.error("1库存不足!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
@ApiOperation("秒杀、并发场景下分布式锁")
@PostMapping("distributedLock2")
public void distributedLock2() {
// 改造成redisson
String lockKey = "lockKey";
// 获取锁对象
RLock lock = redisson.getLock(lockKey);
try {
// 加锁
lock.lock();
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int remainingStock = stock - 1;
redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
log.info("2扣减成功!剩余库存:" + remainingStock);
} else {
log.error("2库存不足!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
}
测试运行结果:
还是100个库存,最后扣减到了0,可以看到没有超卖的情况。
完美解决了秒杀并发超卖的情况。
在一般情况下可以使用第一种改造方法,如果是并发非常高的业务场景下可以使用第二种。
如果觉得不错,可以点赞+收藏或者关注下博主。感谢阅读!