高并发下怎么做余额扣减? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
hhhhhh123
V2EX    程序员

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

  •  
  •   hhhhhh123 2022-11-25 17:10:18 +08:00 8072 次点击
    这是一个创建于 1058 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

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

    41 条回复    2024-11-25 17:43:20 +08:00
    Jooooooooo
        1
    Jooooooooo  
       2022-11-25 17:11:52 +08:00
    单个 user 哪来的高并发?

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

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

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

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

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

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

    改个余额
    加订单入库
    加资金流水能有几张表
    能慢到哪里去啊
    你这个下单接口高峰 qps 能有 1000 吗
    iseki
        30
    iseki  
       2022-11-26 11:30:05 +08:00 via Android
    如果是扣库存,只要保证别 100 个商品,结果 10000 个请求打到数据库上、也别同一时间点 100 个请求全都在数据库上扣同一个商品,就没什么可担心的,数据库的性能足够。
    扣余额就更简单了,限制下不要让一个用户同时发起一堆请求(这本来也该限制吧)
    实现上可以用 update cas ,但存在限制不方便时直接 Serializable 性能不一定差( PostgreSQL )
    love2328
        31
    love2328  
       2022-11-26 11:44:51 +08:00
    @xyjincan 点的时候 没有置等待 ? 要么场景不够经验 要么坑用户
    8520ccc
        32
    8520ccc  
       2022-11-26 11:48:19 +08:00 via iPhone
    @ElmerZhang update xxx set amount = amount - A where amount = B where amount-A>0
    codehz
        33
    codehz  
       2022-11-26 11:49:18 +08:00
    @ElmerZhang 那不如直接 update xxx set amount = amount - A where amount > A (
    chenqh
        34
    chenqh  
       2022-11-26 12:48:53 +08:00
    用 redis 锁不就好了吗?
    vanillacloud
        35
    vanillacloud  
       2022-11-26 12:56:01 +08:00 via iPhone
    我觉得在 update 时 「 where 余额 = 扣款前查询的余额」这一步就能规避重复操作的风险,这不能当 standard procedure 吗?
    noogel
        36
    noogel  
       2022-11-26 13:33:16 +08:00
    高并发扣减:
    1. 合并请求,在保证事务的前提下,将多个扣款请求合并操作,这样只需要做一次锁操作和写操作。
    2. 拆分账户,将热点账户的余额账户拆分成多个子余额账户,以此来降低单个账户扣减操作的并发度。
    3. 使用内存数据库扣减,并异步写日志,所有日志结果可以回溯账户余额结果,和内存数据库做对账。
    LucasLee92
        37
    LucasLee92  
       2022-11-27 11:09:02 +08:00
    @noogel 1 和 2 的处理对热点账户的处理都只考虑怎么解决记账问题
    1 的问题在于合并记账后余额不足的怎么处理,可能拆分记有些还能成功
    2 的问题在于多个账户如何协同管理
    3 的实现最终还是会碰到热点账户问题,当然效率比起数据库来说要好很多了
    不清楚是否有相应的成熟业务的解决方案文章能看看
    hhhhhh123
        38
    hhhhhh123  
    OP
       2022-11-28 09:36:05 +08:00
    @ElmerZhang 我理解 第一步和第二步 ,第三部不是特别理解, 为啥只递归一次?
    hhhhhh123
        39
    hhhhhh123  
    OP
       2022-11-28 09:41:58 +08:00
    @vanillacloud @codehz @8520ccc @richangfan 如果这样的话, 同时有俩个 一个扣款 10 块 一个扣款 5 块。 这样只会执行其中的一个余额。 另外一个就不会执行。 我觉得 8 楼的 第三个条件挺好, 但是递归次数 又不好拿捏。
    hhhhhh123
        40
    hhhhhh123  
    OP
       2022-11-28 09:46:21 +08:00
    我的新思路是 : 只要保证每个请求都是正确的扣钱请求。 然后参考 8 楼, 当然第三个条件只能是一直递归下去, 吧所有的请求都给操作完。
    liangliplusss
        41
    liangliplusss  
       327 天前
    两个方案
    方案一: 悲观锁
    consume(var accountId,var amount) {
    //先查询余额
    "select accountId,balance from xxx where accountId = $accountId for update";
    //计算
    $new_balance = $old_balance - $amount;
    update xxx balance = $new_balance where accountId = $accountId
    }

    方案二: 乐观锁
    consume(var accountId,var amount) {
    flag = false,retires = 3
    // CAS + 重试
    while(!flag && retries > 0) {
    flag = consume0(accountId,amount);
    retries--;
    }

    }

    boolean consume0(var accountId,var amount) {
    //先查询余额(只是查询不加锁)
    "select accountId,balance from xxx where accountId = $accountId";
    //计算
    $new_balance = $old_balance - $amount;
    row = update xxx balance = $new_balance where accountId = $accountId and balance = $old_balance
    return row == 1;
    }

    备选方案:(高并发,单个用户消费并发超过 1000 )缓存 + 消息中间件,
    用户消费操作是扣减缓存中余额(注意这里原子性查询和扣减两个动作,例如 redis 可以使用 lua ), 扣减成功发送消息到消息队列更新数据库。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2484 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 11:28 PVG 19:28 LAX 04:28 JFK 07:28
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86