最近从 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
1
Maboroshii 2020-07-10 17:17:53 +08:00
事务的隔离级别?
|
2
2kCS5c0b0ITXE5k2 2020-07-10 17:19:37 +08:00
上个锁吧 读的是脏数据 并发高肯定会超卖的
|
3
limuyan44 2020-07-10 17:21:16 +08:00
单就 sql 来说没什么问题,这个也不是通过 sql 抛异常事务回滚来控制的,是看 sql 的返回结果是否有更新,没有更新说明无库存来做相应的处理。
|
4
DonaldY 2020-07-10 17:24:09 +08:00
Affected rows: 0,不就是没有更新行数,不满足条件。
异常是要自己抛的,然后才回滚。 update 会锁住行 |
5
AngryPanda 2020-07-10 17:24:29 +08:00
if (affectedRows > 0) {
throw exception; } |
6
pushback 2020-07-10 17:24:51 +08:00
根据 update 返回结果影响行数选择是否回滚
|
7
AngryPanda 2020-07-10 17:24:51 +08:00
if (affectedRows <= 0) {
throw exception; } |
8
chwangtenger OP @limuyan44 对,sql 没问题的,但是影响的行是 0 行,也不会报出异常,所以后面的程序代码里顶多只能自己判断返回的影响行数来判断刚才的减库存有没有成功,但是他代码里也没这么写,我感觉老师不是讲错了。但是我又无法解释为啥他并发的结果是对的,一件都没超。
|
9
wysnylc 2020-07-10 17:26:14 +08:00
数据库解决并发就是扯淡,要么用队列要么 redis incry 要么分布式锁
|
10
AngryPanda 2020-07-10 17:27:34 +08:00
@wysnylc 并发量不十分高的时候,用用还是很香的
|
11
limuyan44 2020-07-10 17:29:57 +08:00
而且你这是个收费课程,连代码都看不到,至少贴段代码出来,你的问题只有上帝知道为什么了。
|
12
wysnylc 2020-07-10 17:32:25 +08:00
@AngryPanda #10 本地搭个 redis 很快,并发问题不在于量大量小而是并发超卖怎么处理超卖 1 个和 100 个没有区别,老板一样会骂你
|
13
AngryPanda 2020-07-10 17:37:17 +08:00
|
14
kanepan19 2020-07-10 17:43:19 +08:00
更新是原子的, 但是你 读取库存的时候,不是原子的.
出库的 manager 启一个事务 最后更新如果库存操作 如果失败,抛异常回滚. 则出货失败. |
15
kanepan19 2020-07-10 17:43:52 +08:00
更正, 以上读取库存的时候肯能是脏数据.
|
16
chwangtenger OP @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); |
17
limuyan44 2020-07-10 19:17:33 +08:00
从代码来看,出现你提问里的情况是正常的现象,对于 createorder 的限制只是在入口的 stock<=0,并发下必然会出现库存为 0 但是订单多了的情况。不过,我看这课程是付费的而且学习人数也不少应该不至于出现这种常识性的错误,不知道是不是老师的最终代码,按理说正常上课会一点一点带着学员修改到正常逻辑的代码。
|
18
cxshun 2020-07-10 19:28:40 +08:00
其实最主要的问题应该是 reduceStock 没有判断返回的条数吧,如果判断一下更新条数再执行后面的创建订单就没问题了。但这种建议还是直接上悲观锁吧,依赖数据库的乐观锁总是不现实的,如果并发量一大,抗不住的。
|
19
xiangyuecn 2020-07-10 19:51:44 +08:00
select xx from xx for update
|
20
sagaxu 2020-07-11 10:46:16 +08:00 via Android
所有付费的编程网课都是智商税
|
21
chwangtenger OP @limuyan44 这已经是最终的代码了。。。
|
22
chwangtenger OP 不好意思啊各位,后面一章里面说到之前讲错了,后来改成了判断一下影响的行数。
|