• 请不要在回答技术问题时复制粘贴 AI 生成的内容
watzds
V2EX  ›  程序员

库存扣减,余额扣减,应该使用乐观锁的方式吗?

  •  
  •   watzds · Apr 29, 2020 · 7443 views
    This topic created in 2205 days ago, the information mentioned may be changed or developed.
    和这种直接扣减的方式相比,有什么好处呢?
    UPDATE stock SET amount = amount - $diff WHERE id=$id AND amount>$diff;

    有些文章说是乐观锁方式可以在重试时保证幂等性,不过什么时候会重试呢
    1. 超时?如果重试又失败,那怎么判断是之前已经成功还是后来竞争失败呢?怎么返回结果给上层呢
    2. 如果是获取版本号一起被重试,那也没什么幂等性了

    总觉得没说服力,五十步笑百步的感觉,想要实现幂等性,最终还是需要其他辅助手段。
    这种方式是有什么其他原因吗,比如性能,正确性?

    说是幂等性优点的(架构师之路): https://mp.weixin.qq.com/s/xXju0y64KKUiD06QE0LoeA

    还有《企业 IT 架构转型之道》这本书里,是将乐观锁扣减方式和 select for update 相比较,没提什么 where 限制扣减,幂等性之类


    大家是用什么方式,这种乐观锁覆盖实现扣减到底有什么好处呢?
    Supplement 1  ·  Apr 29, 2020
    这篇文章也是使用乐观锁方式
    https://vladmihalcea.com/a-beginners-guide-to-database-locking-and-the-lost-update-phenomena/

    查了一些 stackoverflow 上的回答,也有些回答是使用直接扣减和 where 条件限制方式的


    直接扣减确实没法准确获取扣减前后的余额,只能保证余额够扣,不过一般需求也够用了
    乐观锁方式不仅保证余额、库存够扣,而且还限制没被修改过,觉得大多场景过度了

    知乎这个问题下的回答还行
    https://www.zhihu.com/question/61484424

    至于高并发,oceanbase 和 AliSQL 都有提到批量合并提交,不过是在数据库层面做的优化。觉得普通公司在应用层合并也能缓解不少了

    架构师之路这个公众号,挺多文章也不错,不过这几篇不敢苟同
    27 replies    2020-05-06 18:43:08 +08:00
    xuanbg
        1
    xuanbg  
       Apr 29, 2020
    单机系统能不用锁就不要用锁。分布式系统用的也不是单机的锁,要用分布式锁才有用。
    watzds
        2
    watzds  
    OP
       Apr 29, 2020
    @xuanbg #1 所谓乐观锁方式和下面这种不都会有行锁吗,索引正确情况
    UPDATE stock SET amount = amount - $diff WHERE id=$id AND amount>$diff;
    watzds
        3
    watzds  
    OP
       Apr 29, 2020   ❤️ 1
    所谓乐观锁是这种方式:UPDATE t_yue SET money=38, version=$version_new WHERE uid=$uid AND version=$version_old
    xuanbg
        5
    xuanbg  
       Apr 29, 2020
    @watzds UPDATE t_yue SET money=38, version=$version_new WHERE uid=$uid AND version=$version_old 这种方案的问题是并发的情况下只有一个线程能成功,其他线程都会失败。

    数据库的行锁哪能没有呢,正是因为有行锁,对同一条记录进行更新时才会排队。导致后面相同的 sql 会因为 where 中的 version 值变了导致条件不符而失败。而 UPDATE stock SET amount = amount - $diff WHERE id=$id AND amount>$diff;这种方案就不会受影响。
    optional
        6
    optional  
       Apr 29, 2020 via Android
    #3 才是乐观锁,你这个不是。
    optional
        7
    optional  
       Apr 29, 2020 via Android
    @xuanbg version 变了报错才是正常的逻辑,方便事务回滚。
    xmh51
        8
    xmh51  
       Apr 29, 2020   ❤️ 1
    第一个是不太友好的点是你拿不到当时的余额,第二个是你需要对两个字段做关联修改的时候就有问题了
    watzds
        9
    watzds  
    OP
       Apr 29, 2020
    @xmh51 #8 假如需要拿到余额,做一下相关操作之后,再扣款,那乐观锁其实是替换了 for update 倒是有意义的
    比如要求余额是素数才能扣款,那只能 for update 或者乐观锁了

    不过一般余额足够就行,我没想到那样的业务场景
    watzds
        10
    watzds  
    OP
       Apr 29, 2020
    @xuanbg #5 哦,那你是觉得一般应该用 UPDATE stock SET amount = amount - $diff WHERE id=$id AND amount>$diff; 而不是乐观锁是吗?其实我是这么觉得,只是看网上书上都说乐观锁方式比较多
    lhx2008
        11
    lhx2008  
       Apr 29, 2020 via Android
    MYSQL 配合事务可以基本保证幂等性的,超时没事,事务提交不了。语句执行成功就是成功,而且只能执行成功一次。
    lhx2008
        12
    lhx2008  
       Apr 29, 2020 via Android
    而且主要问题是多个事务并发的问题,而不是你自己重试的问题。比如说有个用户点了一下+10,马上又点一下+50,同时到数据库,那就可能有一个会执行失败,要不然就有可能最后只加了 50
    watzds
        13
    watzds  
    OP
       Apr 29, 2020
    @lhx2008 #11 这个我有一点疑问,如果是 commit 发往 数据库,数据库收到了也提交了事务,但是应用没收到网络响应,连接断了,不知应用是怎么处理的,是会超时异常,还是重新建立连接后再次向数据库查询事务是否提交?
    sioncheng
        14
    sioncheng  
       Apr 29, 2020
    乐观锁和幂等性没什么相关性
    watzds
        15
    watzds  
    OP
       Apr 29, 2020
    @lhx2008 #12 这个 sql 并没有并发问题,是能保证正确的,因为 UPDATE 是当前读,会加行数

    UPDATE stock SET amount = amount - $diff WHERE id=$id AND amount>$diff;
    iffi
        16
    iffi  
       Apr 29, 2020
    用乐观锁是为了提高并发性能,如果高并发场景下,你用悲观锁,系统吞吐量就会下降;当然在高并发场景下使用乐观锁,会有很多失败的请求,看你需求场景是否需要支持重试机制。
    lhx2008
        17
    lhx2008  
       Apr 29, 2020
    @watzds #15 我说的就是乐观锁的作用
    @watzds #13 这个问题通常不是很容易发生,可能需要依赖客户端重连之后再去检查。不过只是这一条语句不知道有没有执行成功,不影响后面的执行,因为版本号已经变更了。
    watzds
        18
    watzds  
    OP
       Apr 29, 2020
    @iffi #16 如果是 for update 这种悲观锁,性能影响应该是挺大的,不过这种扣减方式性能和乐观锁会有差别吗?

    UPDATE stock SET amount = amount - $diff WHERE id=$id AND amount>$diff;
    Aresxue
        19
    Aresxue  
       Apr 29, 2020
    乐观锁的本质上是消除锁定, 适用于高并发下 读多(读是无锁)写少 的情况, 用乐观锁就是写也不加锁,然后通过结果去重试, 如果写的请求很多极端点全是写的请求, 那么还不如悲观锁的效率高
    noobsheldon
        20
    noobsheldon  
       Apr 29, 2020
    redis
    bowie
        21
    bowie  
       Apr 29, 2020
    你这种写法都不是什么锁,只是能够保证不会被扣负,所以也不能和 select for update 比较吧,这样写也解决不了并发问题呀
    watzds
        22
    watzds  
    OP
       Apr 29, 2020
    @bowie #21 是的,只保证不会被扣负,不过什么余额、库存业务场景,不被扣负还不够呢?这个经验不多,能举些实际例子吗
    只扣个余额, 要是用 select for update 或者乐观锁,是否 overkill
    watzds
        23
    watzds  
    OP
       Apr 29, 2020
    @lhx2008 #17
    嗯,查了一下 commit 成功,但是返回给客户端失败的情况,应该没有标准处理,不过也极少发生,oracle 倒是有一些机制 Transaction Guard
    https://dba.stackexchange.com/questions/215579/what-happens-if-the-database-nodes-network-fails-just-after-commit-and-before-r?newreg=e21a89d0f6e3489b85a0a4e99ba08c6b
    xmh51
        24
    xmh51  
       Apr 30, 2020
    @watzds 常见的需求 扣款同时添加一个流水记录
    watzds
        25
    watzds  
    OP
       Apr 30, 2020 via Android
    @xmh51 嗯,如果除了扣除数据,还要记录扣款前后数量的话
    bowie
        26
    bowie  
       May 6, 2020
    @watzds 你这个单机是问题不大,如果是正常项目里面就不能这么玩,比如多线程情况下
    线程 1:
    库存:10,扣减 10,剩余库存:0
    线程 2:
    库存:10,扣减 5,剩余库存:5
    这样结果就不对了
    如果并发小的话这种扣减数据库层用乐观锁+保证事务一般就可以了,如果并发很大的话业务层还要用队列和做分布式锁,具体的还是要根据业务场景和系统架构设计来灵活处理
    watzds
        27
    watzds  
    OP
       May 6, 2020 via Android
    @bowie 这个语句当然不只是单机,分布式都能正确,update 是当前读都会加锁的啊
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   6004 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 57ms · UTC 01:57 · PVG 09:57 · LAX 18:57 · JFK 21:57
    ♥ Do have faith in what you're doing.