mysql update 库存解决并发卖超的问题失败了 where and 真的可行吗?

2020-07-10 17:11:30 +08:00
 chwangtenger

最近从 cv 转 java 看一个慕课的教程 《 Java 秒杀系统方案优化 高性能高并发实战》

里面的老师为了解决卖超的问题,用了update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0这样一句话,他说用and stock_count > 0就可以解决库存变负数的问题,从而解决卖超。因为这一操作和后面生成订单的代码在同一个事务的 annotation 里。

可是我实际跑他一样的代码发现订单还是会卖超,。他视频里演示的时候库存设置 10,最终变为 0,订单也刚好 10 个。但我这里虽然库存变成 0 了,订单有 50 多个。

我在数据库试了下是因为执行这句话的时候数据库没有报异常,所以很多线程继续执行了后续的下订单的流程(虽然减库存和下订单在一个事务里,但是那个 where 并没有触发什么错误,where 条件不满足的地方,就 Affected rows: 0,最终没有出发异常和回滚)我解释不了为什么老师测试的结果是对的,我们都是 1000 个线程,他运气没有那么好吧。

另外我还在网上看到了一些回答也是这种类似的方法,用 where and 。。。这不是条件更新吗?就算不满足也不会抛出异常呀,那就不会自动回滚呀。

因为那个课程 2017 年的,我就下了个 mysql5.6 试试,结果也是一样的,有大侠知道为什么吗。

下面是另外一些提到类似这个方法的 https://blog.csdn.net/qq_16504067/article/details/79485443 https://www.v2ex.com/t/254887

3027 次点击
所在节点    MySQL
22 条回复
Maboroshii
2020-07-10 17:17:53 +08:00
事务的隔离级别?
2kCS5c0b0ITXE5k2
2020-07-10 17:19:37 +08:00
上个锁吧 读的是脏数据 并发高肯定会超卖的
limuyan44
2020-07-10 17:21:16 +08:00
单就 sql 来说没什么问题,这个也不是通过 sql 抛异常事务回滚来控制的,是看 sql 的返回结果是否有更新,没有更新说明无库存来做相应的处理。
DonaldY
2020-07-10 17:24:09 +08:00
Affected rows: 0,不就是没有更新行数,不满足条件。

异常是要自己抛的,然后才回滚。

update 会锁住行
AngryPanda
2020-07-10 17:24:29 +08:00
if (affectedRows > 0) {
throw exception;
}
pushback
2020-07-10 17:24:51 +08:00
根据 update 返回结果影响行数选择是否回滚
AngryPanda
2020-07-10 17:24:51 +08:00
if (affectedRows <= 0) {
throw exception;
}
chwangtenger
2020-07-10 17:25:57 +08:00
@limuyan44 对,sql 没问题的,但是影响的行是 0 行,也不会报出异常,所以后面的程序代码里顶多只能自己判断返回的影响行数来判断刚才的减库存有没有成功,但是他代码里也没这么写,我感觉老师不是讲错了。但是我又无法解释为啥他并发的结果是对的,一件都没超。
wysnylc
2020-07-10 17:26:14 +08:00
数据库解决并发就是扯淡,要么用队列要么 redis incry 要么分布式锁
AngryPanda
2020-07-10 17:27:34 +08:00
@wysnylc 并发量不十分高的时候,用用还是很香的
limuyan44
2020-07-10 17:29:57 +08:00
而且你这是个收费课程,连代码都看不到,至少贴段代码出来,你的问题只有上帝知道为什么了。
wysnylc
2020-07-10 17:32:25 +08:00
@AngryPanda #10 本地搭个 redis 很快,并发问题不在于量大量小而是并发超卖怎么处理超卖 1 个和 100 个没有区别,老板一样会骂你
AngryPanda
2020-07-10 17:37:17 +08:00
@wysnylc 当并发数并没有大到可以影响数据库的读写性能,我为啥要改代码?

稍微调整一下 SQL 就解决不可以么?所以我觉得还是得从实际出发。
kanepan19
2020-07-10 17:43:19 +08:00
更新是原子的, 但是你 读取库存的时候,不是原子的.
出库的 manager 启一个事务
最后更新如果库存操作 如果失败,抛异常回滚.
则出货失败.
kanepan19
2020-07-10 17:43:52 +08:00
更正, 以上读取库存的时候肯能是脏数据.
chwangtenger
2020-07-10 17:44:11 +08:00
@limuyan44 代码有好几层,那我贴一下


MiaoshaController.java

@RequestMapping(value="/do_miaosha", method= RequestMethod.POST)
@ResponseBody
public Result<OrderInfo> miaosha(Model model,MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
//判断库存
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);//10 个商品,req1 req2
int stock = goods.getStockCount();
if(stock <= 0) {
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判断是否已经秒杀到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//减库存 下订单 写入秒杀订单
OrderInfo orderInfo = miaoshaService.miaosha(user, goods); //我提问的操作在这个函数里面
return Result.success(orderInfo);
}

MiaoshaService.java 中,上文的 miaoshaService.miaosha

@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
//减库存 下订单 写入秒杀订单
goodsService.reduceStock(goods);
//order_info maiosha_order
return orderService.createOrder(user, goods);
}


GoodsService.java 中,上文的 goodsService.reduceStock

public void reduceStock(GoodsVo goods) {
MiaoshaGoods g = new MiaoshaGoods();
g.setGoodsId(goods.getId());
goodsDao.reduceStock(g);
}


GoodsDao.java 中,上文的 goodsDao.reduceStock
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0")
public int reduceStock(MiaoshaGoods g);


OrderService 上上上段代码最后一句,中的 orderService.createOrder

@Transactional
public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) {
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCreateDate(new Date());
orderInfo.setDeliveryAddrId(0L);
orderInfo.setGoodsCount(1);
orderInfo.setGoodsId(goods.getId());
orderInfo.setGoodsName(goods.getGoodsName());
orderInfo.setGoodsPrice(goods.getMiaoshaPrice());
orderInfo.setOrderChannel(1);
orderInfo.setStatus(0);
orderInfo.setUserId(user.getId());
long orderId = orderDao.insert(orderInfo);

System.out.println();
MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
miaoshaOrder.setGoodsId(goods.getId());
miaoshaOrder.setOrderId(orderId);
miaoshaOrder.setUserId(user.getId());
orderDao.insertMiaoshaOrder(miaoshaOrder);

redisService.set(OrderKey.getMiaoshaOrderByUidGid, ""+user.getId()+"_"+goods.getId(), miaoshaOrder);

return orderInfo;
}



我觉得这两句话虽然在一个事务里,但是第一句里用了 where,虽然库存已经没了,但是没有报异常,导致 return 那句的订单还是可以生成。
//减库存 下订单 写入秒杀订单
goodsService.reduceStock(goods);
//order_info maiosha_order
return orderService.createOrder(user, goods);
limuyan44
2020-07-10 19:17:33 +08:00
从代码来看,出现你提问里的情况是正常的现象,对于 createorder 的限制只是在入口的 stock<=0,并发下必然会出现库存为 0 但是订单多了的情况。不过,我看这课程是付费的而且学习人数也不少应该不至于出现这种常识性的错误,不知道是不是老师的最终代码,按理说正常上课会一点一点带着学员修改到正常逻辑的代码。
cxshun
2020-07-10 19:28:40 +08:00
其实最主要的问题应该是 reduceStock 没有判断返回的条数吧,如果判断一下更新条数再执行后面的创建订单就没问题了。但这种建议还是直接上悲观锁吧,依赖数据库的乐观锁总是不现实的,如果并发量一大,抗不住的。
xiangyuecn
2020-07-10 19:51:44 +08:00
select xx from xx for update
sagaxu
2020-07-11 10:46:16 +08:00
所有付费的编程网课都是智商税

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/688947

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX