单用户余额高并发支出收入有啥好方案?

63 天前
 glaz

比如一个商户一秒一千笔收入记录和一千笔支出记录,咋处理比较好。

4594 次点击
所在节点    程序员
55 条回复
oldking24
62 天前
好像没有看到大家聊到分布式锁的概念 还是因为并发不需要?求解
peyppicp
62 天前
高并发入账:做汇总入账,落完流水走人,定期汇总刷到账户中
高并发下账:做子账户拆分,多个子账户出,会有一些 trade off ,需要取舍
xuanbg
62 天前
@crysislinux 有些情况是不需要保证的,负就负了,难道后面就不能冲正了么?
luckyrayyy
62 天前
@dapang1221 卖货的商品退款,结算和记账是两个事情吧,不涉及主播已结算余额,但是待结算的钱是要扣除的,就有可能有较大并发量的支出。另外平台补贴户之类的也有可能有大量的支出,不过这个比较灵活一般都可以拆成多个。
wxf666
62 天前
@fengYH8080 #40 不是《一次》汇总,是当天《每一笔》都要这么汇总一次,来查余额。。


@sujin190 #39
@fengYH8080 #40

你们觉得,在存储每笔流水时,顺便在这笔流水存当前余额,如何?

根据 5 楼,啊哩云对 MySQL 的测试,读速大约是写速的 3 ~ 4 倍。

因此每写一条流水时,额外读一下最新流水记录,取其中余额,加上本次金额组成最新余额,性能损耗应该不大?

而且,最新流水记录,和即将新生成的流水记录,大概率是临近位置的,应该能利用上 Buffer Pool 里的缓存?所以读损耗进一步减小?



具体来说,主键设成(用户 ID << 42 | 毫秒时间戳),那么添加一笔支出,SQL 大致如下。

要知道是否添加成功,可以检查插入了 0 行还是 1 行。前者大概率是余额不够所致。


```sql
INSERT INTO 流水 (流水 ID, 金额, 这笔流水后用户余额, ...)
SELECT
 (用户 ID << 42 | ${当前毫秒时间戳}),
 ${金额}, -- 支出,应该是负数
 (该用户最新流水记录.这笔流水后用户余额 + ${金额}) AS 该笔支出后余额
FROM (
  SELECT 这笔流水后用户余额
  FROM 流水
  WHERE 流水 ID BETWEEN (用户 ID << 42) AND (((用户 ID + 1) << 42) - 1)
  ORDER BY 流水 ID DESC
  LIMIT 1
) AS 该用户最新流水记录
WHERE 该笔支出后余额 >= 0
```
julyclyde
62 天前
@luckyrayyy 退款又不是实时业务,慢慢退呗
qq135449773
61 天前
很有趣的问题,从来没想过这个问题

如果用队列的话感觉队列的可靠性可能也是一个很大的问题
wangliran1121
61 天前
顺着 @sujin190 的思路,我捋捋

简化的思路,设计两张表,具体做不做 sharding 这里且不讨论

表 1:user_balance ( uid, balance )
表 2:user_transaction (uid, amount , type, create_time)

假定业务接受 T+1 结算余额,那么意味着每日零点会对历史流水做一个汇总计算,汇总结果写入 user_balance 表字段 balance 。

user_balance 表 balance 字段存的是截止至今日的余额,那么意味着今日的支出无论如何都不能大于 balance (用户只能使用当日 0 点以前的余额,接受 T+1 意味着当日一切进账皆被冻结)

接下来收入和支出具体逻辑就是这样的:

收入:
1 、无脑写表 user_transaction

支出:
1 、检查 balance 是否和支出金额相匹配,支出金额不能超过 balance ;
2 、如果支出金额没有超过 balance ,则原子扣减 balance ,扣减后 balance 如果是大于等于 0 ,则写支出流水,以上,扣减和写流水再同一个事务里;

简化思路是这样,但是如果遇到大并发量如何考虑优化方向?

1 、首先支持事务性高性能的 db ,可以首先排除 mysql ,有条件可以往分布式数据库方向选型;
2 、条件有限,考虑将 balance 抽离到 redis ?事务性如何保障?这些细节可以后面考虑
wxf666
61 天前
@wangliran1121 #48

1. 为啥不直接在用户表里,记录实时余额呢?是因为 支出次数 <<< 收入次数,写压力小,还满足风控吗?

2. 23:00 时,用户查看余额,你要汇总当天 1.66 亿条流水,计算余额吗?

3. @sujin190 的思路,在有支出时,user_balance 也是不变的。而是每笔支出,都查 (SELECT SUM(amount) + 该笔支出 FROM user_transaction WHERE uid = ... AND create_time >= 今天) 是否 <= balance 。。

4. 你觉得 45 楼,流水表里记录实时余额,完全免除额外写压力,思路如何?
sujin190
61 天前
@wangliran1121 清算对账也不一定要一天一次,如果流水足够高,一般是需要多次清账对账流程才能保证安全,进一步配合不同层级的风控甚至可以进一步依据风控输出决定每个商户在第几次清账后可以支出

个人其实不赞同使用 redis 保存余额的方案,从支付交易的角度来看安全无风险、准确性可靠性之后才应该考虑效率和性能,毕竟在较高的流水下,任何可能存在的事故一旦出现就可能致命甚至更糟,一分钱的异常和 100 块也毫无区别,常规业务中或许无需考虑某些可能存在的异常,但只要支付流水足够高还是不应该忽略

再说吧,这都真金白银付钱了,高并发也毕竟天花板就在哪吧,否则都那么高流水了分区后加钱加机器加人都是不值一提的,实在没必要在技术工程上冒这个风险吧,毕竟问问老板他也会说获得久才能赚得多
wangliran1121
61 天前
@wxf666

1. 为啥不直接在用户表里,记录实时余额呢?是因为 支出次数 <<< 收入次数,写压力小,还满足风控吗?
--------
我理解,直接用用户表也是需要一个对账过程,增加 T+1 的限制,仅仅是因为给对账留足冗余时间。因此每日或者说定时从明细流水中计算余额这一步操作(对账操作),实际上是不可少的。

2. 23:00 时,用户查看余额,你要汇总当天 1.66 亿条流水,计算余额吗?
--------
定时汇总,自然不必每次都汇总查询当天所有流水,因为 T+1 的限制,只需要查 balance 字段就可以知道余额了,当然实际业务不一定是 T+1 ,这里只是举例子,可以是 10min 延迟,可以是 1min 延迟,看业务可接受度。

3. @sujin190 的思路,在有支出时,user_balance 也是不变的。而是每笔支出,都查 (SELECT SUM(amount) + 该笔支出 FROM user_transaction WHERE uid = ... AND create_time >= 今天) 是否 <= balance 。
--------
每次汇总查在应付大并发读的场景下,肯定不太合适,我理解 @sujin190 他说的尽可能保证正确性的同时再考虑性能优化,首先不可否认,从明细中查询余额的做法,正确性是可以保障的

4. 你觉得 45 楼,流水表里记录实时余额,完全免除额外写压力,思路如何?
--------
这种思路也是可行的,但是要求是数据绝对串行,流水务必一条一条入库,这样最新一条流水即可表示最终余额,如果放到大并发写场景下,也不太合适,总之一切,“看菜吃饭”
wangliran1121
61 天前
@sujin190 是的,redis 会引入更大的系统复杂度和风险,如果业务真这样,其实不需要考虑成本问题,其实金融级别的分布式数据库可以解决这些潜在的事务问题,比如 TiDB 之类的
wangliran1121
61 天前
@wxf666 补充一点,高并发的思路是尽可能少串行化。
wxf666
61 天前
@wangliran1121 #51

1. 是因为害怕,交易过程有 BUG ,会算多余额。失之毫厘,往后谬以千里吗?
所以需要设定,支出上限为昨日余额?那会不会也害怕,今日交易过程也有 BUG 呢。。

2. 用户看余额,应该是《实时》余额吧。。但汇总频率加快成几分钟,应该就不太介意了。。

3. 交易过程只写在一处,甚至写成存储过程,再疯狂并发测试几十上百亿次,可以尽量保证正确性吗?

4. 单个用户是串行,但可以多个用户同时交易吧。。(间隙锁范围,只是该用户现在 ~ 未来?)


5. 现在有点怀疑,会不会只支持串行化,并发数量能更高呢?(免去了很多锁之类的开销?)

我半个月前测试过,SQLite 在 1.3 亿 100 GB 数据时,仍能 1W 随机写事务 / 秒。。

设备是六七年前的轻薄本 + SATA 低端固态,Python 单线程 16 MB 内存完成的。。

源码发在当时的[帖子]( /1075881#reply68 )里了,可以去测试一下。
wangliran1121
61 天前
@wxf666
1 、对于这种交易场景,对账是必须存在的过程,这是一种风控手段,T+1 只是举例子,也可以 T+1min ,所以用户看到的余额清算是稍微不准确的,有些延迟的,这点业务上一般可以接受;
2 、另外,另一种风控要求就是政策和法律,每一笔入账可能都要经过企业内部的风控模型审核完后才能入账,可以是机审,也可以是人审,总之无论政策还是企业都会有这样的要求(换句话说,企业必须要有能力判断一笔账是否异常)
3 、你担心正确性,一般事务性可以保证,但是应付一些极端问题,你通过对账也能修正回来
4 、题目意思就是单个商户高并发读写的场景,因此按照你的设计,只能是串行,设计思路可行,但是于题意而言,不太符合场景;
5 、你测试的只是写请求,但是按照你 45 楼的设计思路,实际上一次入库是需要经历一次完整的读写的,你不读上一笔流水的余额,怎么计算下一笔流水的余额呢?另外,你也不支持并发读,意味着你整套读写过程是串行的,代价十分高昂

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1080273

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX