V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hhhhhh123
V2EX  ›  程序员

高并发下怎么做余额扣减?

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

    这种场景 数据库 是不是只能加锁啊?
    假设 数据库中有两个表 一个是流水表 也就是扣款用, 一个是 userinfo 就是余额在这看
    那么并发场景下。怎么保证余额是>0 且数据无误。

    我的想法是。1 查询余额 如果减去后余额 >=0 则插入扣款后的余额, 这个过程中加锁。 但是这种如果是并发高一点的话是不是很慢啊?

    各位有这块的经验吗? 希望可以指点一下。或者也可以讲解一下你们公司的扣款逻辑是啥? 是如何做的 ?

    40 条回复    2022-11-28 09:46:21 +08:00
    Jooooooooo
        1
    Jooooooooo  
       308 天前
    单个 user 哪来的高并发?

    你想问的是不是库存?
    runningman
        2
    runningman  
       308 天前
    行级锁应该就够你用了。
    dqzcwxb
        3
    dqzcwxb  
       308 天前
    分布式锁,队列,单线程
    hhhhhh123
        4
    hhhhhh123  
    OP
       308 天前
    @runningman 行锁 怎么避免 死锁
    opengps
        5
    opengps  
       308 天前
    一楼说出了核心,虽然银行类业务用户多,但是架不住最大的客户操作完一个交易也是需要时间的,这时间不会导致并发
    hhhhhh123
        6
    hhhhhh123  
    OP
       308 天前
    @Jooooooooo 那库存这种问题 应该怎么解决?
    Jooooooooo
        7
    Jooooooooo  
       308 天前
    @hhhhhh123

    库存的高并发扣减算是比较成熟的东西了, 随便一搜很多的

    比如可以搞多层拦截, 如果你只卖 10 个东西, 要是有 1w 人来抢, 那绝大多数流量没有必要到后端, 反正总是能把东西卖出去的. 前端和网关可以直接随机丢弃流量, 流量到了后端后, 可以再加上 MQ 排队和缓存, 最终再到数据库里行锁扣库存.

    还有手段比如把库存分散到多行数据上, 随机挑一行扣
    ElmerZhang
        8
    ElmerZhang  
       308 天前   ❤️ 6
    如果并发不会很高的话不用在数据库上加锁
    1. 要扣的钱为 A ,先查 amount 当前值为 B ,代码中判断 B >= A
    2. 然后执行 update xxx set amount = amount - A where amount = B
    3. 执行看影响行数,如果为 0 ,重新从第 1 步执行
    一般只需要重试一次。
    dongtingyue
        9
    dongtingyue  
       308 天前
    update xxxxx set xxxx where 余额>xx 余额 用 innodb 本身就有行锁,失败返回异常,这点时间肯定要等的
    coderxy
        10
    coderxy  
       308 天前
    乐观锁就够了,修改时判断一下余额与你之前查到的余额是否一致。
    git00ll
        11
    git00ll  
       307 天前
    一锁 二查 三更新
    CEBBCAT
        12
    CEBBCAT  
       307 天前
    @ElmerZhang
    @dongtingyue
    @coderxy

    看三位的回答中好像没有提到事务,不用事务的话遇到意外停机怎么办呢?或者是我理解错了
    lovelylain
        13
    lovelylain  
       307 天前 via Android
    @CEBBCAT 同时更新多个才要事务,例如给一个人加余额,另一个人减余额。
    awanganddong
        14
    awanganddong  
       307 天前   ❤️ 1
    https://www.51cto.com/article/720873.html

    并发扣款,如何保证一致性
    沈剑 大佬的文章可以看看
    richangfan
        15
    richangfan  
       307 天前   ❤️ 2
    update users set balance = balance - 1 where user_id = 123 and balance >= 1;
    只在余额大于 1 时扣除用户 123 的 1 块钱
    orzwalker111
        16
    orzwalker111  
       307 天前
    @richangfan 假设网关、框架重试,会多扣款,解决手段:
    1 、悲观锁,使用分布式锁
    2 、乐观锁,使用 CAS ,select 得到的 balance 作为 update 的 where 条件,并添加 ver 条件解决 ABA 问题
    xuanbg
        17
    xuanbg  
       307 天前
    不要做无意义的事情,15 楼的方法可以很好的解决 OP 你的这个问题。
    CEBBCAT
        18
    CEBBCAT  
       307 天前
    @jobmailcn 是的。我看楼主这个 case 就是需要一边扣钱,一边发放什么东西。
    louisliu813
        19
    louisliu813  
       307 天前
    @orzwalker111 是的,我们也是使用 cas ,更新时判断 version ,如果被其他事物更新到 version + 1 了,就 select 新的 balance 和 version 出来,然后基于新 version 做判断,新 balance 做更新。
    rqrq
        20
    rqrq  
       307 天前
    try {
    BEGIN;
    SELECT balance FROM userinfo WHERE user_id = xxx FOR UPDATE;
    逻辑判断,有问题就 throw Exception
    UPDATE userinfo...
    COMIT;
    } catch {
    ROLLBACK;
    }
    rqrq
        21
    rqrq  
       307 天前
    BEGIN 写在 try 外面。
    brust
        22
    brust  
       307 天前
    @hhhhhh123 #8
    库存的话
    如果是热点 SKU 可以分段锁 比如有 1w 个库存 可以分成 10 个 1000 出来
    yogogo
        23
    yogogo  
       307 天前
    事务加行锁,扣款交易可以先入库,再用异步任务按顺序执行交易扣款。有些第三方代扣服务就是这样设计的
    dingyaguang117
        24
    dingyaguang117  
       307 天前 via iPhone
    乐观锁即可
    reeco
        25
    reeco  
       307 天前 via iPhone
    实操都是 tcc ,两步提交
    love2328
        26
    love2328  
       307 天前
    你的想法怕慢 实际不会很慢 并发是同等触发 实际触发时并没有的
    xyjincan
        27
    xyjincan  
       307 天前 via Android
    @love2328 有一次网卡,连点很多下,10 冲了 50
    mrpzx001
        28
    mrpzx001  
       307 天前
    用事务不就完事了吗? 怎么都不提事务的?
    8355
        29
    8355  
       307 天前
    用户级别并发锁行锁不就可以了吗

    改个余额
    加订单入库
    加资金流水能有几张表
    能慢到哪里去啊
    你这个下单接口高峰 qps 能有 1000 吗
    iseki
        30
    iseki  
       307 天前 via Android
    如果是扣库存,只要保证别 100 个商品,结果 10000 个请求打到数据库上、也别同一时间点 100 个请求全都在数据库上扣同一个商品,就没什么可担心的,数据库的性能足够。
    扣余额就更简单了,限制下不要让一个用户同时发起一堆请求(这本来也该限制吧)
    实现上可以用 update cas ,但存在限制不方便时直接 Serializable 性能不一定差( PostgreSQL )
    love2328
        31
    love2328  
       307 天前
    @xyjincan 点的时候 没有置等待 ? 要么场景不够经验 要么坑用户
    8520ccc
        32
    8520ccc  
       307 天前 via iPhone
    @ElmerZhang update xxx set amount = amount - A where amount = B where amount-A>0
    codehz
        33
    codehz  
       307 天前
    @ElmerZhang 那不如直接 update xxx set amount = amount - A where amount > A (
    chenqh
        34
    chenqh  
       307 天前
    用 redis 锁不就好了吗?
    vanillacloud
        35
    vanillacloud  
       307 天前 via iPhone
    我觉得在 update 时 「 where 余额 = 扣款前查询的余额」这一步就能规避重复操作的风险,这不能当 standard procedure 吗?
    noogel
        36
    noogel  
       307 天前
    高并发扣减:
    1. 合并请求,在保证事务的前提下,将多个扣款请求合并操作,这样只需要做一次锁操作和写操作。
    2. 拆分账户,将热点账户的余额账户拆分成多个子余额账户,以此来降低单个账户扣减操作的并发度。
    3. 使用内存数据库扣减,并异步写日志,所有日志结果可以回溯账户余额结果,和内存数据库做对账。
    LucasLee92
        37
    LucasLee92  
       306 天前
    @noogel 1 和 2 的处理对热点账户的处理都只考虑怎么解决记账问题
    1 的问题在于合并记账后余额不足的怎么处理,可能拆分记有些还能成功
    2 的问题在于多个账户如何协同管理
    3 的实现最终还是会碰到热点账户问题,当然效率比起数据库来说要好很多了
    不清楚是否有相应的成熟业务的解决方案文章能看看
    hhhhhh123
        38
    hhhhhh123  
    OP
       305 天前
    @ElmerZhang 我理解 第一步和第二步 ,第三部不是特别理解, 为啥只递归一次?
    hhhhhh123
        39
    hhhhhh123  
    OP
       305 天前
    @vanillacloud @codehz @8520ccc @richangfan 如果这样的话, 同时有俩个 一个扣款 10 块 一个扣款 5 块。 这样只会执行其中的一个余额。 另外一个就不会执行。 我觉得 8 楼的 第三个条件挺好, 但是递归次数 又不好拿捏。
    hhhhhh123
        40
    hhhhhh123  
    OP
       305 天前
    我的新思路是 : 只要保证每个请求都是正确的扣钱请求。 然后参考 8 楼, 当然第三个条件只能是一直递归下去, 吧所有的请求都给操作完。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1910 人在线   最高记录 6067   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 10:44 · PVG 18:44 · LAX 03:44 · JFK 06:44
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.