提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
之前写过一个redis防止库存超卖的:
点这里查看
@Resource
private RedisTemplate<String, String> redisTemplate;
@Transactional(rollbackFor = Exception.class)
private boolean reduceStock(String stockId,Integer reduceQuanity) {
// 是否加锁成功
Boolean locked = false;
try {
// 加锁,设置锁超时时间,等待锁的时间
locked = redisLock.tryLock(stockId, 120,20);
if (!locked) {
// 给原来的锁追加时间
redisLock.reLock(stockId, 120);
throw new RuntimeException("当前库存有其他订单在操作");
}
// 库存是否足够
if(getStockQuantity(stockId)>reduceQuanity){
// 扣减库存
int count = reduceQuantity(stockId,reduceQuanity);
if(count==0){
throw new RuntimeException("库存扣减失败");
}
}
return true;
} finally {
// 业务处理成功后释放锁
if (locked) {
redisLock.unlock(stockId);
}
}
}
public Integer getStockQuantity(String stockId){
// select quantity from stock where id = #{stockId}
}
public int reduceQuantity(String stockId,Integer quantity){
// update stock set quantity = quantity - #{quantity} where id = #{stockId}
}
以上代码咋一看确实没啥问题,但是如果我们现在需要下了多个商品需要扣减多个商品的库存会发生什么呢?下面是我们的伪代码
@Transactional(rollbackFor = Exception.class)
private void sumitOrder() {
// 扣减商品a库存
boolean b1 = reduceStock("a",100);
if(!b1){
throw new RuntimeException("库存改变失败");
}
// 扣减商品b库存
boolean b2 = reduceStock("b",2);
if(!b2){
throw new RuntimeException("库存改变失败");
}
}
注意:最大的问题就是,在于并发的情况
。
现在假设:
1.我们有商品a数量有100个。
2.张三和李四都买了多个商品,他们都要买a商品100个。
同时下单的情况, 张三先扣减商品a库存,释放商品a的锁(代码执行到final)、但张三其他商品没有扣减完,redis锁先释放了
但是张三的事务未提交
,此时李四也去扣减库李四获取到redis锁以为库存里面还有数量所以会导致我们库存扣减出错。
数据库修改的时候虽然是有行锁,但是查询的时候没办法保证查询到最新的数据。
实现
解决这个问题也很简单我们只需要给每个库存加上一个版本号,也就是乐观锁
。
CREATE TABLE `stock` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`version` int(11) DEFAULT NULL,
`quantity` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
修改库存需要传入version
,这样即使查询数据不是实时库存,也不会导致我们修改库存时发生错误。
因为修改有行锁,只要进行扣减了库存version = version +1
,并发时候后面的人去扣减库存如果传入的version不是最新,就会提示扣减库存失败,但是这样也会导致一个问题,就是库存充足的情况下明明还有库存,但是由于传入的不是最新的version却会导致扣减库存失败,用户体验度不好。
最终代码如下也不需要redis进行锁定了,可以用jmeter压测
工具进行测试
@Transactional(rollbackFor = Exception.class)
private void sumitOrder() {
// a商品库存版本、从数据库查询出来的
Integer aVersion = 1;
// 扣减商品a库存
boolean b1 = reduceQuantity("a", aVersion ,100);
if(!b1){
throw new RuntimeException("库存改变失败");
}
// b商品库存版本、从数据库查询出来的
Integer bVersion = 111;
// 扣减商品b库存
boolean b2 = reduceQuantity("b", bVersion,2);
if(!b2){
throw new RuntimeException("库存改变失败");
}
}
public int reduceQuantity(String stockId,Integer version ,Integer quantity){
// update stock
// set quantity = quantity - #{quantity} , version =#{version}+1
// where id = #{stockId} and version = #{version} and (quantity - #{quantity})>0
}