前言
本节内容我们主要介绍一下web应用中常见的一类问题——产品“超卖”问题,通过一个springboot项目案例完成超卖现象的演示,并针对不同的应用场景下,提供这一类问题的解决方案,关于更详细的解决方案案例实战内容,请关注作者后期的博客内容。
正文
①创建一个商品库存表,用于商品库存的存储
CREATE TABLE `wms_stock` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`stock_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '仓库编号',
`product_code` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '产品',
`stock_quantity` int DEFAULT NULL COMMENT '库存数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='库存';
②商品库存表中增加一条库存数据用于库存数据的测试
INSERT INTO `wms_stock` (`id`, `stock_code`, `product_code`, `stock_quantity`) VALUES (1, 'A1000', 'P2000', 10000);
③ springboot项目中创建一个WmsStockController类,实现一个扣减库存的请求
package com.ht.atp.plat.controller;
import com.ht.atp.plat.common.Result;
import com.ht.atp.plat.service.WmsStockService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 库存 前端控制器
* </p>
*/
@Api(tags = {"库存管理"})
@RestController
@RequestMapping("/wms/stock")
public class WmsStockController {
@Autowired
private WmsStockService wmsStockService;
@ApiOperation("检查并扣减库存")
@GetMapping("checkAndReduceStock")
public Result checkAndReduceStock() {
wmsStockService.checkAndReduceStock();
return Result.success();
}
}
④ springboot项目中创建一个WmsStockService接口类,实现一个扣减库存的业务接口
package com.ht.atp.plat.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ht.atp.plat.entity.WmsStock;
/**
* <p>
* 库存 服务类
* </p>
*/
public interface WmsStockService extends IService<WmsStock> {
/**
* 检查并扣减库存
*/
void checkAndReduceStock();
}
⑤ springboot项目中创建一个WmsStockServiceImpl实现类,实现一个扣减库存的业务实现
package com.ht.atp.plat.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ht.atp.plat.entity.WmsStock;
import com.ht.atp.plat.mapper.WmsStockMapper;
import com.ht.atp.plat.service.WmsStockService;
import org.springframework.stereotype.Service;
/**
* <p>
* 库存 服务实现类
* </p>
*/
@Service
public class WmsStockServiceImpl extends ServiceImpl<WmsStockMapper, WmsStock> implements WmsStockService {
@Override
public void checkAndReduceStock() {
// 查询库存
WmsStock wmsStock = baseMapper.selectById(1L);
// 验证库存大于0再扣减库存
if (wmsStock != null && wmsStock.getStockQuantity() > 0) {
wmsStock.setStockQuantity(wmsStock.getStockQuantity() - 1);
baseMapper.updateById(wmsStock);
}
}
}
⑥启动测试项目,通过浏览器访问扣减库存接口,库存由10000正常扣减为9999
⑦将扣减的库存恢复为10000,使用jmeter压测工具并发测试。添加一个线程组:并发100循环100次,即10000次请求
⑧给线程组添加HTTP Request请求
⑨添加扣减库存接口配置
⑩添加压测报告
⑪开始使用jmeter压测
测试结果:请求总数10000次,平均请求时间130ms,中位数(50%)请求是在83ms内完成的,错误率0%,每秒钟平均吞吐量754次。库存还没有扣减的为9808。此时就产生了超卖现象。实际库存应该为0。
⑫对于该并发问题我们使用jvm的synchronized本地加锁或者使用ReentrantLock加锁
⑬ 恢复库存数据为10000,再次使用jmeter工具压测
测试结果:请求总数10000次,平均请求时间332ms,中位数(50%)请求是在304ms内完成的,错误率0%,每秒钟平均吞吐量296次。库存扣减为0。本地加锁确实解决了并发的问题。可以从测试结果中发现,本地加锁确实可以解决“超卖的问题”,同时也会降低接口访问请求的吞吐量,由于加锁的原因。
⑭本地加锁存在的问题
对于单应用而言,本地加锁确实能够解决并发访问的问题。我们知道本地锁只能对本jvm虚拟机内是有效的,对于分布式多服务应用而言,为了高可用高并发往往会将单个应用部署为多个服务节点,这个时候,本地锁只能对本服务有效,依然会有并发的问题。那么对于多服务的应用该如何解决并发问题呢,这就要用到分布式锁了,关于分布式锁的相关内容实战,请关注我的后期的内容。
结语
对于“超卖”这一类问题,我们只能通过加锁的方式控制并发访问。在实际开发过程中,对于单体应用场景而言,我们可以使用本地锁实现并发访问控制,对于分布式微服务应用的场景中,我们可以使用分布式锁,控制并发访问。在后续的内容中,我们通过具体的技术解决方案,实现并发问题的解决。