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
kikione
V2EX  ›  MySQL

mysql 减库存并发问题

  •  
  •   kikione · 316 天前 · 4286 次点击
    这是一个创建于 316 天前的主题,其中的信息可能已经有所发展或是发生改变。

    减库存数量为 num

    update mytable set inventory = inventory-num WHERE id= 1 and inventory >=num

    这样减库存不用加乐观锁和悲观锁,也可以是吧

    但是有什么弊端吗

    第 1 条附言  ·  316 天前
    一个库 ,一张表
    第 2 条附言  ·  316 天前
    默认隔离级别下
    47 条回复    2021-11-23 10:55:05 +08:00
    sadfQED2
        1
    sadfQED2  
       316 天前 via Android
    可能减成负的呗
    vvhhaaattt
        2
    vvhhaaattt  
       316 天前 via Android
    除非事务隔离是串行化,否则怎么看都会有问题吧
    查询跟修改操作分两步走,并发时应该会库存少减,超卖吧
    vvhhaaattt
        3
    vvhhaaattt  
       316 天前 via Android
    @vvhhaaattt 不对,单条语句似乎会加行锁,可能这么写也没问题?
    zjsxwc
        4
    zjsxwc  
       316 天前
    有效解决并发数据一致性问题,我能想到就两种方式:
    一种是用增量式记录的方式,每次数据获取(比如这里的库存)其实是根据历史记录“算”出来的,这里可以通过缓存中间计算、定时归档等方式加速“算”的速度,缺点是占用空间比较大。

    一种就是单一队列方式,就是排队序列化处理,缺点是时间长速度慢。
    dqzcwxb
        5
    dqzcwxb  
       316 天前
    跨库失效,多表失效
    要解决并发只能是 redis 或者队列串行处理
    bigbyto
        6
    bigbyto  
       316 天前 via iPhone
    为啥都在顾左右而言他?我认为这条是没问题的,你那条 sql 会给 id 为 1 的索引加 X Lock ,其他事务想修改必须等你释放锁,因此在默认的隔离级别下是可以保证一致性的。

    除了 READ_UNCOMMITED 不行,其他隔离级别应该都没问题。
    zjsxwc
        7
    zjsxwc  
       316 天前
    @bigbyto
    数据库确实没问题,
    但感觉业务上会出错,楼主这条 sql 就表示数据库能减去库存就减去库存值,但卖我仍旧业务上照样卖,这在业务上好像不能接受。
    sagaxu
        8
    sagaxu  
       316 天前 via Android
    @zjsxwc 业务是知道减库存失败的,为什么还要照卖?
    zjsxwc
        9
    zjsxwc  
       316 天前
    @sagaxu 哦哦 ,可以 通过 查看执行后 affected rows count 判断是否失败
    ```
    update mytable set inventory = inventory-num WHERE id= 1 and inventory >=num
    ```
    huang119412
        10
    huang119412  
       316 天前
    update 本来就可以当成分布式锁。直接更新不就是悲观锁?缺点是并发低,竞争大。而且你要理解 rows affected ,rows matched 的区别。
    mazyi
        11
    mazyi  
       316 天前 via iPhone
    update 就是锁呀,实现业务没问题,就是可能效率有问题,所以才想那么多 cache 提前处理
    IvanLi127
        12
    IvanLi127  
       316 天前 via Android
    我感觉没啥问题
    markgor
        13
    markgor  
       316 天前
    該說的上面都說了,
    num 只要前面有做判斷一般都沒問題,(如傳入負數)。
    Jooooooooo
        14
    Jooooooooo  
       316 天前
    这条语句没问题

    就是效率会有问题, 一万个人同时来减就卡死了

    一般都会放在缓存里面过一道
    documentzhangx66
        15
    documentzhangx66  
       316 天前
    真正的生产系统,业务流程代码,需要考虑:

    1.健壮性
    比如数据库、子业务系统突然崩溃;比如机房停电。

    2.可调试性
    某个环节出了 bug ,系统设计上需要做到在最短时间内,通过某种方式进行 bug 追踪。

    3.记录日志
    比如细化的日志级别,连 if else 或 switch case 走的是哪条分支,都需要记录。

    以上这些功能,在流程设计与实现时,还需要考虑事务成功与失败时的处理方案,需要考虑并行时如何按顺序操作资源来确保不发生死锁,需要考虑如果某个环节出现了性能瓶颈应该如何处理,等等。

    这就是为什么真正健壮的系统,一个简单的业务操作,数据库的业务 SQL 都可以有上千行的原因。
    leafre
        16
    leafre  
       316 天前
    这句话就是乐观锁
    缺点就是高并发大部分都是失败,一般不采用
    什么你说不是高并发?那为什么用锁
    fgwmlhdkkkw
        17
    fgwmlhdkkkw  
       316 天前
    @documentzhangx66 😅😅😅😅😅😅😅😅😅😅😅
    kikione
        18
    kikione  
    OP
       316 天前
    @Jooooooooo 没有那么高的并发,同时有 10 个人就不错了
    kikione
        19
    kikione  
    OP
       316 天前
    @leafre 也不是高并发,但是 update 不是都带锁么,不是我用不用锁的问题吧
    jsdi
        20
    jsdi  
       316 天前
    没啥问题 但是 update 是当前读,会加悲观锁,也就是写锁,效率上会有问题
    kikione
        21
    kikione  
    OP
       316 天前
    @mazyi 并发不超过十个,所以没有缓存也可以吧
    leafre
        22
    leafre  
       316 天前
    @kikione 不是高并发性能没问题,字段再上个 unsigned int 保底
    as9567585
        23
    as9567585  
       316 天前
    乐观锁
    encro
        24
    encro  
       316 天前
    应该没有问题,
    如果不相信可以用`select balance from user for update`这样的,
    性能也没有任何问题。
    bucketcheng
        25
    bucketcheng  
       316 天前   ❤️ 1
    read commit 模式以上都没有问题,主要是看你事务大小,你如果事务时间比较耗时,而且并发比较高的话,可能话等锁超时,就看并发量
    bucketcheng
        26
    bucketcheng  
       316 天前
    都是操作这条库存行的前提
    gosidealone
        27
    gosidealone  
       316 天前
    @bucketcheng 对的,我公司和楼主也是一模一样的处理方法,然后事务耗时,并发高,就出问题了。有什么好的解决方法吗
    bucketcheng
        28
    bucketcheng  
       315 天前
    @gosidealone 比较简单方式是 把更新这个冲突最频繁的放在最后更新,减少行锁时间,其次是减少事务粒度,及合并更新,在就架构上做改造,改动就比较大了,我们在这方面做了挺多工作的,
    sujin190
        29
    sujin190  
       315 天前   ❤️ 1
    sql 没问题,但是实际业务中意义及用处不大,正常的订单中不可能只买一件商品,以及下单过程中还有优惠折扣促销处理等等的流程,这样即意味着大量的回滚流程且开事务时间过长,效率不会很高,而且多件下单的时候很容易导致死锁问题
    gosidealone
        30
    gosidealone  
       315 天前
    @bucketcheng 那我想问下,放在最后更新为什么能减少行锁时间?因为事务没提交完就一直锁着吗
    sujin190
        31
    sujin190  
       315 天前
    @gosidealone #30 必然的啊,否则事务用来干嘛的,所以实际业务中多件商品下单这种操作如果不排序,分分钟死锁
    xuanbg
        32
    xuanbg  
       315 天前
    sql 没问题,扯东扯西各种问题的,大概都是在 show 自己懂得多吧,好为人师果然是一种人性。好吧,我从业务角度来说我对减库存的理解。

    其实,除了特殊商品,一般来说就是数量有限,譬如车票、二手货等不能超卖,正常的商品谁管你超卖多少啊。生产 /采购部门自然会把你卖掉的货生产出来 /采购回来补上库存缺口。
    myd
        33
    myd  
       315 天前
    没问题,不会超卖。
    xe2vforesu
        34
    xe2vforesu  
       315 天前
    没问题,不会多卖,也不会少卖。但是高并发场景下存在锁竞争,死锁检测等性能问题,进而导致系统处理能力降低。
    Chad0000
        35
    Chad0000  
       314 天前 via iPhone
    我的方案是直接使用 queue ,一个消费者负责增减库存。库存在 Redis 中,带版本号。读从 Redis 中,写同时写。版本号在写时加入判断可确保不会有脏数据。

    queue 性能还可以,一般你的系统不会一秒上万修改库存请求。

    以上方案已在生产环境使用
    love2020
        36
    love2020  
       314 天前
    你需要考虑并发修改为负数、ABA 问题(版本),幂等性。 通常可以采用 SELECT & SET & CAS 解决。
    jorneyr
        37
    jorneyr  
       314 天前
    没问题,MySQL 有行锁
    sujin190
        38
    sujin190  
       314 天前
    @Chad0000 #35 那你这如何和下单流程的事务做绑定呢,先减库存的话,万一你这下单最后又失败了,你这库存如何恢复,后减库存的话,中间有时间差,也不能保证不超卖吧
    Chad0000
        39
    Chad0000  
       314 天前
    @sujin190 支付前可以 Lock 库存。支付成功后出库失败,则订单需要自己处理取消逻辑,达到最终一致即可。
    sujin190
        40
    sujin190  
       314 天前
    @Chad0000 #39 不,我说的只是下单失败的情况,按这个逻辑,Lock 库存需要在商品有效性检查的库存校验时就要完成,而实际业务中下单后续商品还有其它校验流程,接着还有地址检查,优惠券营销促销折扣,风控校验等等一系列过程,这些都是要做一致性绑定的,更不要说后面的支付流程了
    Chad0000
        41
    Chad0000  
       314 天前
    @sujin190 #40

    下单最后又失败了:订单系统做最终一致性检查,对于失败的已经出库的,及时通知库存回库,简单讲你下单后,生成一个若干分钟后的任务检查是否完成支付和出库。下单前已经锁定库存,所以有时间差也不会导致超卖除非锁库超时自动退回。

    超卖问题:这个无法完全避免,你总会在各个流程遇到意外情况:比如拖着不付款或付款时间太长的,导致锁库超时已返回库存。这时下单使用的就是未锁定的库存 - 不能保证有货。付款锁定和库存锁定这两个东西即使一致也无法保证出库,因为付款可以因为第三方支付而拖时间。所以都只能满足大部分场景,然后通过最终一致化完成少数不正常的。
    shanghai1943
        42
    shanghai1943  
       314 天前
    不考虑高并发的话,这种实现方式简单,只要判断 affected rows 是否等于 1 就行了。
    haython
        43
    haython  
       314 天前
    淘宝以前就是这么干的
    hangszhang
        44
    hangszhang  
       314 天前
    没问题,就是性能不高
    dgr007
        45
    dgr007  
       314 天前
    没问题,性能很好的
    可能需要解决的是单条记录热点更新问题
    bsg1992
        46
    bsg1992  
       313 天前
    SQL 没问题
    你这个就是乐观锁,弊端就是并发量大的话会导致更新失败。
    特别是针对某条热点数据会一直失败,这个需要解决一下。
    一般这种情况你看的场景
    1.是否允许超卖,不允许超卖的话另开一个监控程序发现超卖就直接退款 /或者不发货 当然这个是业务层面上的解决方案。
    2.用队列进行消费,看你消费端的速度了。
    3.就是实时计算库存数量,根据消耗库存记录算出来剩余库存
    hui314
        47
    hui314  
       313 天前
    不想改动的话,有个最简单方法,新增一个字段 version,减库存 sql:update mytable set inventory = inventory-num , version = version+1 WHERE id= 1 and inventory >=num and version = {version};
    即使并发也只有一个能成功,但是这样的话,只能一个人成功下单。
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2199 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 49ms · UTC 13:53 · PVG 21:53 · LAX 06:53 · JFK 09:53
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.