本篇讲解单体应用中实现扣减库存,分别使用synchronized和ReentrantLock实现锁,不会出现扣除超卖的现象。
库存表
-- test.stock definition
CREATE TABLE `stock` (
`stock_id` int NOT NULL AUTO_INCREMENT COMMENT '库存主键',
`product_id` int NOT NULL COMMENT '商品ID',
`stock_num` int DEFAULT NULL COMMENT '库存数量',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(100) DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(100) DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`stock_id`),
KEY `stock_product_id_IDX` (`product_id`) USING BTREE,
CONSTRAINT `stock_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`product_id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='库存表';
数据
INSERT INTO test.stock
(stock_id, product_id, stock_num, create_time, create_by, update_time, update_by)
VALUES(6, 6, 5000, '2022-09-06 15:19:18', 'elite', '2022-09-06 15:19:18', 'elite');
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.1version>
dependency>
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.6version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.1.4version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.76version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.27version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.11version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
server:
port: 8899
##配置应用的名字
spring:
application:
name: CurrentcyLockApplication
profiles:
active: dev
##配置数据库
datasource:
##mysql配置
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://ip:3306/test
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
@SpringBootApplication
public class CurrentcyLockApplication
{
public static void main( String[] args )
{
SpringApplication.run(CurrentcyLockApplication.class,args);
}
}
/**
* 库存映射类
*/
@Mapper
public interface StockMapper extends BaseMapper<Stock> {
@Select("SELECT stock_id, product_id, stock_num, create_time, create_by, update_time, update_by\n" +
"FROM test.stock where product_id = #{productId}")
Stock getStockByProductId(Integer productId);
}
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("`stock`")
public class Stock {
/**
* 用户ID
*/
@TableId(value = "stock_id", type = IdType.AUTO)
private Integer stockId;
/**
* 商品ID
*/
@TableField(value = "product_id")
private Integer productId;
/**
* 库存数量
*/
@TableField(value = "stock_num")
private Integer stockNum;
//创建人
@TableField(value = "create_by")
private String createBy;
//创建时间
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(value = "create_time",fill = FieldFill.INSERT)
private String createTime;
//更新人
@TableField(value = "update_by")
private String updateBy;
//更新时间
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(value = "update_time",fill = FieldFill.INSERT_UPDATE)
private String updateTime;
}
@RestController
@RequestMapping("/stock")
public class StockController {
@Autowired
IStockService stockService;
/**
* 扣减库存
* @return
*/
@GetMapping("/deductStock/{productId}")
public String deductStock(@PathVariable("productId") Integer productId){
return stockService.deductStock(productId);
}
}
/**
* 库存处理的逻辑
*/
public interface IStockService extends IService<Stock> {
String deductStock(Integer productId);
}
@Service
@Slf4j
public class IStockServiceImpl extends ServiceImpl<StockMapper, Stock> implements IStockService {
@Autowired
StockMapper stockMapper;
/**
* 扣减库存
* @return
*/
@Override
public String deductStock(Integer productId) {
log.info("扣减库存开始......");
Stock stock = stockMapper.getStockByProductId(productId);
log.info("当前库存量:" + stock.getStockNum());
//扣减库存
if (stock.getStockNum() > 0) {
//默认扣减1
stock.setStockNum(stock.getStockNum() - 1);
stockMapper.updateById(stock);
} else {
log.info("扣减库存失败,无库存可用......");
return "扣减库存失败";
}
return "ok";
}
}
库存5000,使用jemeter发送100个线程循环50次进行扣减库存。
此时库存4863,出现了超卖的情况。
方法上加上关键字synchronized
public synchronized String deductStock(Integer productId)
synchronized 了独占的排他锁,只能一个一个线程进行执行。
private final ReentrantLock lock = new ReentrantLock();
/**
* 扣减库存
* @return
*/
@Override
public /*synchronized*/ String deductStock(Integer productId) {
log.info("扣减库存开始......");
//幂等性
//尝试获取锁:
// 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
if (lock.tryLock()) {
try {
//处理重复通知
//接口调用的幂等性:无论接口被调用多少次,以下业务执行一次
//获取库存数量
Stock stock = stockMapper.getStockByProductId(productId);
log.info("当前库存量:" + stock.getStockNum());
//扣减库存
if (stock.getStockNum() > 0) {
//默认扣减1
stock.setStockNum(stock.getStockNum() - 1);
stockMapper.updateById(stock);
} else {
log.info("扣减库存失败,无库存可用......");
return "扣减库存失败";
}
}finally {
lock.unlock();
}
}
return "ok";
ReentrantLock是可重入锁,请不到锁的情况下返回false,不必一直等待。
本篇讲解都是JDk自身提供的工具来实现锁,对于分布式部署的服务并不适用。分布式服务需要用分布式锁来实现,如数据库来实现,或者redis来实现分布式锁。