V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
watzds
V2EX  ›  程序员

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

  •  
  •   watzds · 2020-04-29 09:56:54 +08:00 · 6586 次点击
    这是一个创建于 1698 天前的主题,其中的信息可能已经有所发展或是发生改变。
    和这种直接扣减的方式相比,有什么好处呢?
    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 限制扣减,幂等性之类


    大家是用什么方式,这种乐观锁覆盖实现扣减到底有什么好处呢?
    第 1 条附言  ·  2020-04-29 16:34:46 +08:00
    这篇文章也是使用乐观锁方式
    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 条回复    2020-05-06 18:43:08 +08:00
    xuanbg
        1
    xuanbg  
       2020-04-29 10:16:22 +08:00
    单机系统能不用锁就不要用锁。分布式系统用的也不是单机的锁,要用分布式锁才有用。
    watzds
        2
    watzds  
    OP
       2020-04-29 10:23:36 +08:00
    @xuanbg #1 所谓乐观锁方式和下面这种不都会有行锁吗,索引正确情况
    UPDATE stock SET amount = amount - $diff WHERE id=$id AND amount>$diff;
    watzds
        3
    watzds  
    OP
       2020-04-29 10:25:04 +08:00   ❤️ 1
    所谓乐观锁是这种方式:UPDATE t_yue SET money=38, version=$version_new WHERE uid=$uid AND version=$version_old
    xuanbg
        5
    xuanbg  
       2020-04-29 11:05:26 +08:00
    @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  
       2020-04-29 11:10:24 +08:00 via Android
    #3 才是乐观锁,你这个不是。
    optional
        7
    optional  
       2020-04-29 11:12:37 +08:00 via Android
    @xuanbg version 变了报错才是正常的逻辑,方便事务回滚。
    xmh51
        8
    xmh51  
       2020-04-29 11:16:20 +08:00   ❤️ 1
    第一个是不太友好的点是你拿不到当时的余额,第二个是你需要对两个字段做关联修改的时候就有问题了
    watzds
        9
    watzds  
    OP
       2020-04-29 11:31:45 +08:00
    @xmh51 #8 假如需要拿到余额,做一下相关操作之后,再扣款,那乐观锁其实是替换了 for update 倒是有意义的
    比如要求余额是素数才能扣款,那只能 for update 或者乐观锁了

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

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

    UPDATE stock SET amount = amount - $diff WHERE id=$id AND amount>$diff;
    Aresxue
        19
    Aresxue  
       2020-04-29 12:00:05 +08:00
    乐观锁的本质上是消除锁定, 适用于高并发下 读多(读是无锁)写少 的情况, 用乐观锁就是写也不加锁,然后通过结果去重试, 如果写的请求很多极端点全是写的请求, 那么还不如悲观锁的效率高
    noobsheldon
        20
    noobsheldon  
       2020-04-29 13:34:17 +08:00
    redis
    bowie
        21
    bowie  
       2020-04-29 16:08:11 +08:00
    你这种写法都不是什么锁,只是能够保证不会被扣负,所以也不能和 select for update 比较吧,这样写也解决不了并发问题呀
    watzds
        22
    watzds  
    OP
       2020-04-29 16:38:05 +08:00
    @bowie #21 是的,只保证不会被扣负,不过什么余额、库存业务场景,不被扣负还不够呢?这个经验不多,能举些实际例子吗
    只扣个余额, 要是用 select for update 或者乐观锁,是否 overkill
    watzds
        23
    watzds  
    OP
       2020-04-29 17:54:47 +08:00
    @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  
       2020-04-30 09:56:44 +08:00
    @watzds 常见的需求 扣款同时添加一个流水记录
    watzds
        25
    watzds  
    OP
       2020-04-30 12:20:50 +08:00 via Android
    @xmh51 嗯,如果除了扣除数据,还要记录扣款前后数量的话
    bowie
        26
    bowie  
       2020-05-06 18:02:48 +08:00
    @watzds 你这个单机是问题不大,如果是正常项目里面就不能这么玩,比如多线程情况下
    线程 1:
    库存:10,扣减 10,剩余库存:0
    线程 2:
    库存:10,扣减 5,剩余库存:5
    这样结果就不对了
    如果并发小的话这种扣减数据库层用乐观锁+保证事务一般就可以了,如果并发很大的话业务层还要用队列和做分布式锁,具体的还是要根据业务场景和系统架构设计来灵活处理
    watzds
        27
    watzds  
    OP
       2020-05-06 18:43:08 +08:00 via Android
    @bowie 这个语句当然不只是单机,分布式都能正确,update 是当前读都会加锁的啊
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2918 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 13:52 · PVG 21:52 · LAX 05:52 · JFK 08:52
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.