这种场景 数据库 是不是只能加锁啊?
假设 数据库中有两个表 一个是流水表 也就是扣款用, 一个是 userinfo 就是余额在这看
那么并发场景下。怎么保证余额是>0 且数据无误。
我的想法是。1 查询余额 如果减去后余额 >=0 则插入扣款后的余额, 这个过程中加锁。 但是这种如果是并发高一点的话是不是很慢啊?
各位有这块的经验吗? 希望可以指点一下。或者也可以讲解一下你们公司的扣款逻辑是啥? 是如何做的 ?
1
Jooooooooo 2022-11-25 17:11:52 +08:00
单个 user 哪来的高并发?
你想问的是不是库存? |
2
runningman 2022-11-25 17:15:17 +08:00
行级锁应该就够你用了。
|
3
dqzcwxb 2022-11-25 17:21:12 +08:00
分布式锁,队列,单线程
|
4
hhhhhh123 OP @runningman 行锁 怎么避免 死锁
|
5
opengps 2022-11-25 17:30:31 +08:00
一楼说出了核心,虽然银行类业务用户多,但是架不住最大的客户操作完一个交易也是需要时间的,这时间不会导致并发
|
6
hhhhhh123 OP @Jooooooooo 那库存这种问题 应该怎么解决?
|
7
Jooooooooo 2022-11-25 17:40:58 +08:00
@hhhhhh123
库存的高并发扣减算是比较成熟的东西了, 随便一搜很多的 比如可以搞多层拦截, 如果你只卖 10 个东西, 要是有 1w 人来抢, 那绝大多数流量没有必要到后端, 反正总是能把东西卖出去的. 前端和网关可以直接随机丢弃流量, 流量到了后端后, 可以再加上 MQ 排队和缓存, 最终再到数据库里行锁扣库存. 还有手段比如把库存分散到多行数据上, 随机挑一行扣 |
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 步执行 一般只需要重试一次。 |
9
dongtingyue 2022-11-25 17:43:43 +08:00
update xxxxx set xxxx where 余额>xx 余额 用 innodb 本身就有行锁,失败返回异常,这点时间肯定要等的
|
10
coderxy 2022-11-25 18:15:32 +08:00
乐观锁就够了,修改时判断一下余额与你之前查到的余额是否一致。
|
11
git00ll 2022-11-25 18:46:54 +08:00
一锁 二查 三更新
|
12
CEBBCAT 2022-11-25 21:09:09 +08:00
|
13
lovelylain 2022-11-25 21:29:26 +08:00 via Android
@CEBBCAT 同时更新多个才要事务,例如给一个人加余额,另一个人减余额。
|
14
awanganddong 2022-11-25 22:02:53 +08:00 1
|
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 块钱 |
16
orzwalker111 2022-11-25 22:34:35 +08:00
@richangfan 假设网关、框架重试,会多扣款,解决手段:
1 、悲观锁,使用分布式锁 2 、乐观锁,使用 CAS ,select 得到的 balance 作为 update 的 where 条件,并添加 ver 条件解决 ABA 问题 |
17
xuanbg 2022-11-25 22:36:05 +08:00
不要做无意义的事情,15 楼的方法可以很好的解决 OP 你的这个问题。
|
19
louisliu813 2022-11-25 23:17:01 +08:00
@orzwalker111 是的,我们也是使用 cas ,更新时判断 version ,如果被其他事物更新到 version + 1 了,就 select 新的 balance 和 version 出来,然后基于新 version 做判断,新 balance 做更新。
|
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; } |
21
rqrq 2022-11-26 01:20:13 +08:00
BEGIN 写在 try 外面。
|
23
yogogo 2022-11-26 08:09:52 +08:00
事务加行锁,扣款交易可以先入库,再用异步任务按顺序执行交易扣款。有些第三方代扣服务就是这样设计的
|
24
dingyaguang117 2022-11-26 08:29:52 +08:00 via iPhone
乐观锁即可
|
25
reeco 2022-11-26 08:46:13 +08:00 via iPhone
实操都是 tcc ,两步提交
|
26
love2328 2022-11-26 08:53:33 +08:00
你的想法怕慢 实际不会很慢 并发是同等触发 实际触发时并没有的
|
28
mrpzx001 2022-11-26 09:22:29 +08:00
用事务不就完事了吗? 怎么都不提事务的?
|
29
8355 2022-11-26 10:13:21 +08:00
用户级别并发锁行锁不就可以了吗
改个余额 加订单入库 加资金流水能有几张表 能慢到哪里去啊 你这个下单接口高峰 qps 能有 1000 吗 |
30
iseki 2022-11-26 11:30:05 +08:00 via Android
如果是扣库存,只要保证别 100 个商品,结果 10000 个请求打到数据库上、也别同一时间点 100 个请求全都在数据库上扣同一个商品,就没什么可担心的,数据库的性能足够。
扣余额就更简单了,限制下不要让一个用户同时发起一堆请求(这本来也该限制吧) 实现上可以用 update cas ,但存在限制不方便时直接 Serializable 性能不一定差( PostgreSQL ) |
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
|
33
codehz 2022-11-26 11:49:18 +08:00
@ElmerZhang 那不如直接 update xxx set amount = amount - A where amount > A (
|
34
chenqh 2022-11-26 12:48:53 +08:00
用 redis 锁不就好了吗?
|
35
vanillacloud 2022-11-26 12:56:01 +08:00 via iPhone
我觉得在 update 时 「 where 余额 = 扣款前查询的余额」这一步就能规避重复操作的风险,这不能当 standard procedure 吗?
|
36
noogel 2022-11-26 13:33:16 +08:00
高并发扣减:
1. 合并请求,在保证事务的前提下,将多个扣款请求合并操作,这样只需要做一次锁操作和写操作。 2. 拆分账户,将热点账户的余额账户拆分成多个子余额账户,以此来降低单个账户扣减操作的并发度。 3. 使用内存数据库扣减,并异步写日志,所有日志结果可以回溯账户余额结果,和内存数据库做对账。 |
37
LucasLee92 2022-11-27 11:09:02 +08:00
@noogel 1 和 2 的处理对热点账户的处理都只考虑怎么解决记账问题
1 的问题在于合并记账后余额不足的怎么处理,可能拆分记有些还能成功 2 的问题在于多个账户如何协同管理 3 的实现最终还是会碰到热点账户问题,当然效率比起数据库来说要好很多了 不清楚是否有相应的成熟业务的解决方案文章能看看 |
38
hhhhhh123 OP @ElmerZhang 我理解 第一步和第二步 ,第三部不是特别理解, 为啥只递归一次?
|
39
hhhhhh123 OP @vanillacloud @codehz @8520ccc @richangfan 如果这样的话, 同时有俩个 一个扣款 10 块 一个扣款 5 块。 这样只会执行其中的一个余额。 另外一个就不会执行。 我觉得 8 楼的 第三个条件挺好, 但是递归次数 又不好拿捏。
|
40
hhhhhh123 OP 我的新思路是 : 只要保证每个请求都是正确的扣钱请求。 然后参考 8 楼, 当然第三个条件只能是一直递归下去, 吧所有的请求都给操作完。
|
41
liangliplusss 24 天前
两个方案
方案一: 悲观锁 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 ), 扣减成功发送消息到消息队列更新数据库。 |