秒杀系统库存
今天带来一套秒杀库存扣减
涉及到单体架构和集群架构 希望能给你们带来帮助 我也不想学 但是bgs不教
首先讲一下大致的扣减库存的思路
@RequestMapping("deduct_stock")
public CommonResult deductStock(){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("shop_stock"));
if (stock>0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("shop_stock",realStock + "");
System.out.println("库存扣减成功 剩余库存"+realStock);
return CommonResult.ok();
}else{
System.out.println("库存扣除失败 ,库存不足");
return CommonResult.fail(888,"库存扣除失败 ,库存不足");
}
}
// 这大致就是一个扣减库存的思路 先去判断是否还有库存 然后再去扣减一个库存 替换掉原有的库存数量
// 下面开始说一下利弊
// 利:这样不会出现库存为负数的情况
// 弊:如果高并发的情况下 多用户可能会造成同时下单同一个编号的商品 (仅次于超卖)
下面说一下这种情况的解决方案
@RequestMapping("deduct_stock")
public CommonResult deductStock(){
synchronized (this){ // 我们在这里加上一条同步锁 当一个请求执行完毕之后 下一个请求才能进入
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("shop_stock"));
if (stock>0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("shop_stock",realStock + "");
System.out.println("库存扣减成功 剩余库存"+realStock);
return CommonResult.ok();
}else{
System.out.println("库存扣除失败 ,库存不足");
return CommonResult.fail(888,"库存扣除失败 ,库存不足");
}
}
}
// 这样虽然可以解决重复下单 解决超卖问题
// 但是也仅限于单体服务器架构
// 如果是集群架构 就避免不了同时下单的问题
那我们再次优化一下多集群架构重复下单的问题
@RequestMapping("deduct_stock")
public CommonResult deductStock(){
String lockKey = "lock_key";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock"); //setnx分布式锁
if (!result){
return CommonResult.fail(999,"拥挤中-稍后重试");
}else{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("shop_stock"));
if (stock>0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("shop_stock",realStock + "");
System.out.println("库存扣减成功 剩余库存"+realStock);
stringRedisTemplate.delete(lockKey); //把锁删除
return CommonResult.ok();
}else{
System.out.println("库存扣除失败 ,库存不足");
stringRedisTemplate.delete(lockKey); //把锁删除
return CommonResult.fail(888,"库存扣除失败 ,库存不足");
}
}
}
// 虽然多集群不在单机 但是共用一个redis的情况下就不会出现重复下单 使用缓存中间件对其进行一个约束
// 虽然代码到这里已经可以实现了分布式锁 但是还是有问题 下面我们再次优化一下
再次优化代码
@RequestMapping("deduct_stock")
public CommonResult deductStock(){
String lockKey = "lock_key";
// 设置一个同步锁过期时间 防止系统挂掉
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock",10,TimeUnit.SECONDS);
if (!result){
return CommonResult.fail(999,"拥挤中-稍后重试");
}else{
try{ // 防止中级出现异常
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("shop_stock"));
if (stock>0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("shop_stock",realStock + "");
System.out.println("库存扣减成功 剩余库存"+realStock);
return CommonResult.ok();
}else{
System.out.println("库存扣除失败 ,库存不足");
return CommonResult.fail(888,"库存扣除失败 ,库存不足");
}
}finally { // 不管是否有异常都必须执行
stringRedisTemplate.delete(lockKey);
}
}
}
到目前为止 代码已经比较完善了 但是还是会有部分问题 在高并发的场景下 如果自己的锁被别人释放的时候 也可能会出现重复下单问题 这个问题也就是出现在finally里面 所以我们最好还是多加一个判断 并且将唯一标识存起来
@RequestMapping("deduct_stock")
public CommonResult deductStock(){
String lockKey = "lock_key";
String clientId = String.valueOf(UUID.randomUUID()); // 创建下单人的唯一标识
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId,10,TimeUnit.SECONDS);
if (!result){
return CommonResult.fail(999,"拥挤中-稍后重试");
}else{
try{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("shop_stock"));
if (stock>0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("shop_stock",realStock + "");
System.out.println("库存扣减成功 剩余库存"+realStock);
return CommonResult.ok();
}else{
System.out.println("库存扣除失败 ,库存不足");
return CommonResult.fail(888,"库存扣除失败 ,库存不足");
}
}finally {
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){ // 如果是这个人结束
stringRedisTemplate.delete(lockKey); // 才去释放这个锁 如果不是这个人的时候 不释放这个锁
}
}
}
当到这里的时候 代码量已经比刚开始多了很多了 并且也解决了一个又一个问题 但是在这里还是会有一个问题 当我们代码执行到finally的时候 如果前面的时间已经过了9秒 并且finally中又卡顿了1秒或者2秒 在高并发的情况下 会造成自己的锁已经超时释放了 然后finally执行之后删除了后面那个人的锁 这种问题也是会影响到订单重复的问题
这里就介绍一下Redisson了 如名 这也就是redis的son
依赖部分
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.8</version>
</dependency>
启动类部分
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
// 其他部分通过@Autowired注入就行了。。
当注入了Redisson之后 我们的代码又可以减少了
@RequestMapping("deduct_stock")
public CommonResult deductStock(){
String lockKey = "lock_key";
// 获取锁对象
RLock redissonLock = redisson.getLock(lockKey);
try{
//加锁
redissonLock.lock();
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("shop_stock"));
if (stock>0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("shop_stock",realStock + "");
System.out.println("库存扣减成功 剩余库存"+realStock);
return CommonResult.ok();
}else{
System.out.println("库存扣除失败 ,库存不足");
return CommonResult.fail(888,"库存扣除失败 ,库存不足");
}
}finally {
// 解锁
redissonLock.unlock();
}
}
下图也就是加上redisson之后的流程图
~~~~~~~~~~~~~~~~~~~~~~~ 如果看完对你有帮助 还望一键三连~~~~~~~~~~~~~~~~~~~~~~~