V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
MySQL 5.5 Community Server
MySQL 5.6 Community Server
Percona Configuration Wizard
XtraBackup 搭建主从复制
Great Sites on MySQL
Percona
MySQL Performance Blog
Severalnines
推荐管理工具
Sequel Pro
phpMyAdmin
推荐书目
MySQL Cookbook
MySQL 相关项目
MariaDB
Drizzle
参考文档
http://mysql-python.sourceforge.net/MySQLdb.html
chwangtenger
V2EX  ›  MySQL

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

  •  
  •   chwangtenger · 2020-07-10 17:11:30 +08:00 · 2993 次点击
    这是一个创建于 1587 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近从 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

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

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

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

    稍微调整一下 SQL 就解决不可以么?所以我觉得还是得从实际出发。
    kanepan19
        14
    kanepan19  
       2020-07-10 17:43:19 +08:00
    更新是原子的, 但是你 读取库存的时候,不是原子的.
    出库的 manager 启一个事务
    最后更新如果库存操作 如果失败,抛异常回滚.
    则出货失败.
    kanepan19
        15
    kanepan19  
       2020-07-10 17:43:52 +08:00
    更正, 以上读取库存的时候肯能是脏数据.
    chwangtenger
        16
    chwangtenger  
    OP
       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
        17
    limuyan44  
       2020-07-10 19:17:33 +08:00
    从代码来看,出现你提问里的情况是正常的现象,对于 createorder 的限制只是在入口的 stock<=0,并发下必然会出现库存为 0 但是订单多了的情况。不过,我看这课程是付费的而且学习人数也不少应该不至于出现这种常识性的错误,不知道是不是老师的最终代码,按理说正常上课会一点一点带着学员修改到正常逻辑的代码。
    cxshun
        18
    cxshun  
       2020-07-10 19:28:40 +08:00
    其实最主要的问题应该是 reduceStock 没有判断返回的条数吧,如果判断一下更新条数再执行后面的创建订单就没问题了。但这种建议还是直接上悲观锁吧,依赖数据库的乐观锁总是不现实的,如果并发量一大,抗不住的。
    xiangyuecn
        19
    xiangyuecn  
       2020-07-10 19:51:44 +08:00
    select xx from xx for update
    sagaxu
        20
    sagaxu  
       2020-07-11 10:46:16 +08:00 via Android
    所有付费的编程网课都是智商税
    chwangtenger
        21
    chwangtenger  
    OP
       2020-07-11 20:08:18 +08:00
    @limuyan44 这已经是最终的代码了。。。
    chwangtenger
        22
    chwangtenger  
    OP
       2020-07-11 21:25:46 +08:00
    不好意思啊各位,后面一章里面说到之前讲错了,后来改成了判断一下影响的行数。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4686 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 04:04 · PVG 12:04 · LAX 20:04 · JFK 23:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.