开启左侧

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

[复制链接]
在线会员 16hxxW4 发表于 2023-1-14 07:46:08 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
那里念分外道二面
1.下并收,纯真数据库update革新账户余额,数据库的update止锁已经成为瓶颈。
2.扣加要包管不克不及扣为正数。
ps:二个典范的场景
1.扣费,企业账户收流质大概白包,用户报到付出。今朝尔理解到的计划即是分多账户低落单账户的压力大概革新改拔出 同步来计较余额。
2.充值,挨赐给主播,计划类似。

精彩评论5

avatar
在线会员 e6fNY0ENR 发表于 2023-1-14 07:46:54 | 显示全部楼层
余额操作在大多数系统都是不可缺少和不允许出现问题的   如何修改余额 , 这个问题可能在实际项目中 没那么简单;

如何修改余额

假设一个用户数据   :   
id⇒12 | user_name⇒放放  |  fee⇒ 30  | updated_at ⇒ 2019-09-06 15:51:33
修改余额
  1. //消费金额
  2. $spend = 10;
  3. //查询用户余额
  4. $user = select id,fee from `users` where id = 12;
  5. //计算金额
  6. $newFee = $user['fee']-$spend;
  7. //.. 这里检查余额是否足够
  8. //更新余额
  9. update `users` set fee = $newFee  where id = 12 ;
复制代码
按照正常逻辑来说上面更新余额是没问题的
但是如果发生并发  当 A 跟  B  同时发起请求时
请求A  查询出余额为 30
请求B 查询出余额为 30
请求B 更新余额为20
请求A 更新余额为20
最终用户余额为  20
也就是说 用户余额为30  两个请求都消费了  10元  即  30 - 10 -10 =10  但是当发生并发请求时  余额最终为20   这里的金额是错误的
在请求B 修改了  余额之后  请求A的查询出来的余额已经不是正确的了 导致了  余额更新错误
常见的解决办法  添加数据库的行锁

当请求A 执行时  先加入锁  阻塞  请求B直到  请求A完成之后  请求B  才继续执行
当然使用事务 并不是那么明智
  1. //开始事务
  2. begin;
  3. //消费金额
  4. $spend = 10;
  5. //查询用户余额
  6. $user = select id,fee from `users` where id = 12 for update;
  7. //计算金额
  8. $newFee = $user['fee']-$spend;
  9. //.. 这里检查余额是否足够
  10. //更新余额
  11. update `users` set fee = $newFee where id = 12  ;
  12. //确认成功之后  提交事务
  13. commit
复制代码
CAS  业务层面乐观锁

什么是  CAS
在更新的时候使用初始值(即查询出来的当前余额)作为条件 compare 限制只有当初始值没有改变时才允许更新成功 set
Compare And Set(CAS)
使用该方式  修改  更新语句
  1. //消金额
  2. $spend = 10;
  3. //查询用户余额
  4. $user = select id,fee from `users` where id = 12;
  5. $oldFee = $user['fee'];
  6. //计算金额
  7. $newFee = $user['fee']-$spend;
  8. //.. 这里检查余额是否足够
  9. //更新余额
  10. update `users` set fee = $newFee   where id = 12 and fee = $oldFee ;
复制代码
这里如果发生同时修改产生并发  将只有一边修改成功    这时候如果产生失败 可以对他进行重试等操作
为什么不使用  减等于  的sql语句

例如  :
update users set fee = fee - $spend where id = 12 ;
这里要再加上余额的判断避免出现  负数金额
update users set fee = fee - $spend where id = 12 and fee >= $spend ;
稍微改一下这里的更新  语句 也能完成正确的更新 就算是并发也都将正常
但是这样做将产生一个问题   不幂等  
什么是不幂等  ?
在相同的条件下 , 执行同一请求,得到的结果相同才符合幂等性
  1. 也即是说     
  2. fee = fee - $spend  不幂等
  3. fee = $newFee         幂等
复制代码
不幂等的情况下 如果发生重复执行的情况将产生重复扣款
事实上实际业务如何正确的扣款 根据业务的实际情况 可能需要注意更多细节 , 越大的业务量,需要面对更多的问题处理更多的细节. 以上的方案也仅仅是最基础的处理 实际情况需要更多的耐心和思考 共勉之 ~
以上.
插曲
参考资料
[1] 58 沈剑 公众号 : 架构师之路
并发扣款一致性,幂等性问题,这个话题还没聊完!!!
[2] 其他文章推荐
TweLveAndOne:如何进行余额更新,并发扣款,数据一致性问题TweLveAndOne:为什么超级签名也会掉?TweLveAndOne:使用 VM VirtualBox 搭建完善的Linux开发环境有哪些软件堪称神器,却不为大众所知?
回复

使用道具 举报

avatar
在线会员 vpbBB 发表于 2023-1-14 07:47:36 | 显示全部楼层
取决于是计费高并发,还是用户高并发。
前者是同一账户,同时并发多个计费请求,导致余额变化,好像一个银行账户,多个银行卡,同时刷卡买东西;后者是多个用户在线,用各自的账户消费,互不影响。
看题设,更像是前者,就基于计费高并发来讨论。
曾遇到过一个需求,每个账户每秒有几百次计费请求,要求很简单,
1,又快又准。
2,余额不为负。
同一个字段被并发修改,很自然会想到用lock,但系统有很多其他业务逻辑,计费只是很小的一部分,要足够的轻,就开始考虑尽量无锁的方案。大概思路如下,
记录持久化,余额内存化。
余额是充值和消费的结果,在不断变化,但充值和消费是记录,一旦发生,不会再变,某时某刻花了10块,这条记录产生了,就永远不会变。这类记录持久化,放在DB里。
有了记录,可以在任何时刻,重建余额。这个余额是否需要持久化,不一定,还要考虑是否存在过期等。我们虽选择持久化余额,但不加锁,因为读写不发生在DB上,而是在内存里。
内存里,用户有两个值,一个是余额,一个是花费。用户消费时,余额不变,花费增加。两个问题,
为什不直接减余额呢?
不改余额,就可以保证内存里的余额始终和DB中的一致,而内存里花费始终和消费记录一致。用户的实时余额 = 余额 - 花费。
内存计费是否加锁?
余额不为负,意味着要先确认实时余额 > 所需花费,才能消费,check, then update,这并不是atomic 的,意味着存在 race condition,计费函数是不是一定要加锁呢?
如果先查余额,再扣钱,的确要加锁;但也可以先扣钱,再查余额,若小于0,则把钱加回来,返回计费失败,阻止消费,这样就不用加锁了。当然,余额和花费应选Atomic数据类型。
这样高并发下的余额扣减就变得非常的轻,对 performance 几乎没有影响,也满足了又快又准的需求。
回复

使用道具 举报

avatar
在线会员 jCgT25J 发表于 2023-1-14 07:48:36 | 显示全部楼层
作为普通公司,就不说那些分布式各种云了,进入常人模式:
1、容量评估
高峰期的时候到底多少用户来扣费抢红包,几点存在热点数据吗?
压力到了多少,系统开始可能会挂(你可以通过限流等模式,当然体验打折扣)?

2、找出关键代码进行调优
步骤1:
正常的红包扣减流程是:加锁、查询、扣减、释放,但是lock记录过程,并发度下降了,这个就导致并发下降。
通常情况下是先查询(一次网络交互),再发更新语句(一次网络交互),代码层面感觉不出,在高并发下有实质的区别。
优化:
如果一次性的网络交互与数据库的交互就完成上面两个动作,那么并发就提高了。
所以可以通过数据库的where条件自己直接判断,符合要求就直接在数据库里面update更新(例如执行更新以后总金额大于100就直接返回0,然后在把成功与否的信息返回给我们,这样每秒的吞吐量tps就可以提高很多很多。例如从1000提高到4000.
当然你还可以采用其他办法,批量,缓存等,目的是减少数据库网络交互。

步骤2:
通过步骤1优化,tps到了4000,感觉比较难上去了,这个时候可以进行【水平扩张】。
优化:
将红包记录单独出来,1000块钱的红包类似于分布式一样分为10个100块的请求分布到10个不同的记录去。由原来的一个行锁分布到10个去,1个进程分布到10个去。
更新的能力优化:从业务逻辑或者代码层面优化,减少更新频率
查询的能力优化:索引优化(活动id加到where里面,建立索引),子活动做一些前缀索引之类的,like查询之类的减少查询频率,通过一些优化尽量不让访问。
这样tps可以从4000提高到了20000

步骤3:
热点问题的话:经常处理是进行【拆分】。可以使用内存模式、队列技术进行再进一步提高tps,有些规则匹配可以放到内存中。(数据量尽量少,维度可做一些拆解)

最后:压测这些吞吐量,继续优化,整合。
回复

使用道具 举报

avatar
在线会员 awvm1 发表于 2023-1-14 07:48:56 | 显示全部楼层
mark 下明天来写答案

今天已经是明天了。
高频更新当中,一般来讲,有几个现成的解决方案。
第一,记住,任何性能问题,都可以用钱来解决,硬件加强,上更好的服务器,更好的数据库,更好的中间件总是能解决的,足够大的硬件,加上内存数据库,减少IO,总是能解决的。这点上,我们是穷人,我们先跳过
第二,我们尝试从架构上解决这个问题,那么,高频更新的情况,如何做?你现在是单表单字段,锁表或者锁行是避免不了的,最好的结果也是锁行,当然你可以采用一些索引,消极锁来一定程度上加快,但是效果有限。那么有什么更好的解决方案?这点上大学本科教科书已经给出了答案:空间换时间。首先,高频操作你要做精算分析,比如1秒响应10万次,那么单次最低扣减额根据正态分布需要大约不超过总余额的十万分之一,否则有很大的概率几秒就扣减完了,那你就这个高频也没有意义了。这点上,题目并没有明确写出,所以我们只做理论研究,假设余额足够大,扣减能维持在高频下极大时间。
那么具体怎么做?将你的扣减字段拆分,假设你原来的字段为T1.F1,那么现在我们设置Tx.F1,令 sum(T2.F1:Tx.F1)=T1.F1看到这里,你已经懂了,既然我扣减都发生在一个字段上,为什么我不把这个字段拆分成多个字段?比如我10个人用勺去一个桶里舀水,为什么我不把这桶水分成5个子桶?这样的话,我原来10个串行操作,就变成了5个并行的2步串行,速度就会快上大于4倍,小于五倍。为什么不是等于五倍?因为你的子桶每次更新以后,如果小于0,那么还需要去总桶里申请金额。
假设你的余额有100K,你设置10个分表,那么你的每个分表设置多少余额?最简单的做法是10K,然后扣减,扣减的操作随机fit到这10个分表,任何分表扣减额度小于等于零,那么再随机向某一大于零的分表offset,并且关闭这个分表,直到最后一个分表用完。这个架构还可以改进优化,这里留给你做一道思考题。简单提示2点,1,操作不应该随机fit分表,2,完全并行不如星型结构好

好,空间换时间是一种方案,我们还有另外一种方案,假设由于种种理由数据库并不能分,那么我们有什么办法?有,上一种方法,是高频访问单一资源,所以我们增加资源,那么这一种方法,我们降频。降频的情况很简单,我们考虑到,比如每X次扣减,当总额小于余额的时候,扣减一次余额和扣减X次余额业务含义是一样的。这很好理解,你去超市买东西,不会每件东西给一次钱,而是加总给钱的,所以我们不必每次扣减都操作数据库,你可以band扣减,在余额尚足的情况下,你总是能成功的,如果你band100次扣减为一个单位,你的性能就提升了大约90倍。那么如何处理某一次导致余额不足的扣减呢?你这么聪明,看到这里已经懂了这是第二个思考题了,思路是一样的。

如果你把这两种方案组合起来用,会有什么效果?
回复

使用道具 举报

avatar
在线会员 VeUB7M 发表于 2023-1-14 07:49:06 | 显示全部楼层
单独的余额扣减没什么问题,因为这个操作只涉及到一个账号,简单地在数据库上用transaction做就行。
如果数据库成为瓶颈,那就把账户表切片,每个片只涉及到一部分用户就行了。
问题是单账户的扣减很少会发生,绝大多数涉及到钱的系统里金额变化的操作都是两个账户一进一出,而且这组操作从业务角度来说是一个transaction,如果你把账户表切片就不可避免会有进出两个账户不在同一片里的情况。
在这种情况下,如果你不想让分布式事务成为整个系统的瓶颈,可以采用类似银行跨行转账的方式,先在转出账户扣减,然后在转入账户入账,每一步操作都有一个日志,如果第二步出现问题则回退第一步操作。
这种方式会有短暂的数据不一致,但可以保证最终一致性,而且不会把账户余额扣减成负数。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册 qq_login

本版积分规则

发布主题
阅读排行更多+
用专业创造成效
400-778-7781
周一至周五 9:00-18:00
意见反馈:server@mailiao.group
紧急联系:181-67184787
ftqrcode

扫一扫关注我们

Powered by 职贝云数A新零售门户 X3.5© 2004-2025 职贝云数 Inc.( 蜀ICP备2024104722号 )