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

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

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

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

    55 条回复    2024-10-17 09:30:59 +08:00
    mooyo
        1
    mooyo  
       37 天前
    合并批量提交?
    yinmin
        2
    yinmin  
       37 天前 via iPhone
    通过消息队列扔给多个微服务子系统并发处理
    kirory
        3
    kirory  
       37 天前
    一个商户一个表,每个记录一行数据
    cooltechbs
        4
    cooltechbs  
       37 天前 via Android
    假如有 N 个虚拟服务节点,每个商户就分 N 行,每个服务实例的请求汇总到其中一行,再加一个定时更新的整体汇总行
    wxf666
        5
    wxf666  
       37 天前
    @glaz 用高性能机子,直接在数据库上操作,可行吗?


    啊哩云的 MySQL 测试[^1]说:

    - 1 核 1G 机器,MySQL 能 6200 读 / 秒,1800 写 / 秒。
    - 16 核 64G 机器,MySQL 能 7.6W 读 / 秒,2.2W 写 / 秒。

    第一种小机子,支持你 2 个类似商户,每秒千次收付款,
    第二种大机子,支持你 22 个。。


    [^1]: https://help.aliyun.com/zh/rds/support/test-results-of-apsaradb-rds-instances-that-run-mysql-8
    xuanbg
        6
    xuanbg  
       37 天前   ❤️ 11
    哪有什么余额记录,从来都只有流水记录。余额都是在需要的时候,从流水中汇总出来的。无论你并发有多高,只要数据库能顶住写入就行。
    ymmud
        7
    ymmud  
       37 天前
    内存库
    csys
        8
    csys  
       37 天前 via Android
    如果你的数据库能比较好的做到单行高并发,可以测下能不能满足要求,我记得是有相关数据库方案的

    不过最好还是在应用来做,在应用实现方案这边来解决这个问题,不然业务规模扩大后很难处理,这样的话技术难度会比较高一些
    https://suraciii.github.io/posts/hot-spot-balance-reduce

    类似#1 所说的合并批量提交,拿延迟和失败率换并发吞吐
    把多个交易一起结账落库,用 actor 来控制并发冲突
    csys
        9
    csys  
       37 天前 via Android
    这里有个简单的数学题,如果你每次处理一个交易需要 1 毫秒,那么在最理想情况下,一个商户 1 秒就只能处理 1000 个交易,实际上远远不到

    所以就不要“每次处理一个交易”
    crysislinux
        10
    crysislinux  
       37 天前 via Android
    @xuanbg 没余额怎么保证不扣成负的?
    isnullstring
        11
    isnullstring  
       37 天前
    消息队列

    数据库内做减法
    lcy630409
        12
    lcy630409  
       37 天前
    前端综合处理 前端给一个时差出来
    比如 等待 转圈圈,1-2s 一般都能忍受
    jimrok
        13
    jimrok  
       37 天前
    从券商这段的系统设计来看,在报盘的时候就验证余额可用后才报单给交易所,最终账户的日终清算是从中登那边的交割记录和交易所的流水记录一起清算的。所以盘中只是对一个临时账户进行控制,并不是最终账户的数据落地。
    InDom
        14
    InDom  
       37 天前
    实时余额通过 redis / 内存 储存计算, 初步计算通过的交易记录丢队列. 然后前端等待后端处理结果后再返回.

    后端不间断扫描,每次取走积压的一批处理完毕后任务交还给队列, 前端拿到结果返回.

    哪怕后端每秒扫描一次,也足够用了.
    sujin190
        15
    sujin190  
       37 天前 via Android   ❤️ 7
    6 楼说的对,这种商户收款付款的打开收支,应该流水优先,一般设计中这种场景都是需要有对账清算的流程的,所以为了提高获取余效率,可以把余额分成已清算余额和未清算余额,已清算的余额可以在对账清算时更新,未清算余额也实时通过流水获取,流水都是不可改的

    关于余额扣负这个问题,单个账号流水很大的,大多数系统都存在未清算金额不可以支出的限制,这个既是技术处理的困难,同时也是你还要过风控不可以立即支出,否则反洗钱啥的法律问题叔叔分分钟找上你

    对账清算可以自动也可以手动,单账户高并发大量流水没有清算对账无论从技术上看还是从法律上看都是不现实的
    qweruiop
        16
    qweruiop  
       37 天前
    @InDom 高手的意思,需要做减法和检查余额够不够的时候,这个余额都丢 redis ,然后流水记录丢 db ,是这样嘛?
    jorneyr
        17
    jorneyr  
       37 天前
    架构简单,逻辑简单,更多的硬件支持。

    高并发的事情尽量避免复杂设计。
    yc8332
        18
    yc8332  
       37 天前
    根本不可能有你说的这种问题。。。高并发最终就是队列处理
    jonsmith
        19
    jonsmith  
       37 天前 via Android
    瓶颈在哪里?如果是数据库,就上 redis 、消息队列
    InDom
        20
    InDom  
       37 天前
    @qweruiop #16 不全对, redis 是先过滤明确不合理的请求(可能导致余额为负数的请求), 如果符合条件, 才会进入 db 层的计算, 一批一批的处理,而不是一个一个的处理.

    最终数据还是以 db 为准, 另外, 我也是菜鸡, 这个方案也是我臆想的, 没有实际项目支撑.
    fengYH8080
        21
    fengYH8080  
       37 天前
    @crysislinux 他这个没有余额的意思是不存储于持久化数据库中,也就不存在频繁的余额变更数据库操作,只有流水的写入操作,在需要的时候通过流水汇总出来余额这个值,就能在内存里根据这个余额去做逻辑限制。
    两种设计方式适用不同性质的系统,我之前就设计过不需要记录余额的系统,处理逻辑会稍微复杂点,好处就是系统中只有流水这一个概念
    snitfk
        22
    snitfk  
       37 天前
    同一个商户在一秒内这么高的并发?这是给量化服务吗?你们这系统就只服务一个商户?如果商户量增加了,你这架构要处理的量级就马上上升了。
    8355
        23
    8355  
       37 天前
    不要瞎想,这种业务就是流水插库,你只需要保证执行顺序符合预期就行,使用消息队列进行消费即可。
    收入是流水表字段正数
    首先你要从客观上去理解这个业务,按正常来说是收入大于支出还是支出大于收入,不可能两个一直差不多数量。
    交易类的业务会有一个结算周期,不可能实时结算,包括提现都是有周期的,就是为了减少因为银行/网络延迟等客观原因导致的延迟存在。
    ForMrFang
        24
    ForMrFang  
       37 天前
    @xuanbg 交易时实时从流水汇总查询余额的话,会不会比较慢.
    wxf666
        25
    wxf666  
       37 天前
    @fengYH8080 #21 每一笔支出,都需要余额吧?否则咋知道,能否继续花钱呢?


    @sujin190 #15 意思是说,只能花已清算账目后余额内的钱吗?

    如果每天清算一次,清算后还剩 1W 块,第二天可以花无数笔 < 1W 元的支出?

    还是说,23:00 的一笔支出,需要计算( 00:00 ~ 22:59 的余额 + 已清算余额)>= 支出金额,才能花钱?

    按楼主所说,每秒 1000 笔收入 / 支出,那该笔支出,就要算当天 1.66 亿次交易,得出未清算余额???


    sujin190
        26
    sujin190  
       37 天前
    @wxf666 #25 只能花已经清算的余额 1w ,也就是<=1w 的钱,这个就是账期,1 天其实大多数商户都能接受

    如果你做的是收单那风控和清算是必需的,否则你很可能会面临法律风险
    如果你只是对接支付宝微信支付,这么大流水清算对账也是应该考虑的,虽然风控的事情支付宝微信帮你干了,但是毕竟我们自己的服务器和微信支付宝并不是在一起的,万一程序有点啥未知 bug ,那就有可能直接要破产倒闭了还可能面临债务
    通过清算对账延迟一点对大家都好,商户看到的余额反正是实时的,并不会收这个账期影响,只是并不能立刻支出这部分钱罢了,而且现实中正常交易都是要么小额收入大额支出,要么大额收入小额支出,小额大量收入同时支出的貌似大都不是正常生意哈
    fengYH8080
        27
    fengYH8080  
       37 天前
    @wxf666 #25 可以简单理解为余额从持久化数据库抽到了内存中,可以解绑掉流水 + 余额的数据库层原子操作。看到这种设计第一印象都会觉得每次汇总一个人的余额非常耗资源,很多处理方式都可以避免的,简单点的可以通过中间表固定时间汇总好之前的余额,汇总实时余额只要这个时间点的余额记录和这个时间点之后的流水做汇总就好了。所以才说这个设计逻辑会复杂点。
    julyclyde
        28
    julyclyde  
       37 天前
    @crysislinux 确实不能保证不扣成负的
    所以信用卡有所谓超限费(罚款)这么个项目
    guanhui07
        29
    guanhui07  
       37 天前
    高并发最终就是队列处理 顺序 消费 慢慢排队
    Jackm
        30
    Jackm  
       37 天前
    收付款用微信支付宝呀,他们有提供接口。像这种级别的给别人交点手续费就交点吧。
    dapang1221
        31
    dapang1221  
       37 天前
    高并发收入可以理解,但高并发支出……?比如高并发 10 笔的峰值,那这 10 笔其实可以排队在 1 秒内处理完成。但如果说连续 1 小时,每秒里都有上百笔支出……真的有这种场景吗,除了券商高频交易这种
    ivvei
        32
    ivvei  
       37 天前
    这也不多啊,你要设计啥?
    wxf666
        33
    wxf666  
       37 天前
    @fengYH8080 #27 《汇总实时余额 = 上次汇总余额记录 + 上次汇总时间点之后的流水累计余额》,

    像上面所说,每天汇总一次的话,23:00 时,当天有 1.66 亿 笔未汇总流水。那计算一次余额的代价,是不是太大了。。



    @sujin190 #26 是第二天内,能花无数笔 <= 1W 的钱吗?还是累计最多 1W 的钱?

    前者不可接受。后者怎么实现呢?每一笔支出,都检查当天流水吗?像上面所说,23:00 时,当天有 1.66 亿 笔流水,这。。

    另外,商户看到的余额,也是(已清算 1W 余额 + 当天 1.66 亿笔流水余额)吗。。这。。
    fengpan567
        34
    fengpan567  
       37 天前
    kafka 消费
    sujin190
        35
    sujin190  
       37 天前
    @wxf666 #33 你这个提的就没道理好吧,当天能有 1.66 亿笔流水么!!每天数百亿上千亿的流水?瞎想可不行,真有这么多,清算对账流程还需要比这复杂得多得多,我说的这种能可靠风险低每天处理数百万千万级流水就不错了,想着小学数学解决登月这就不显然扯淡么
    每天几万笔可以用余额加减,这种上亿笔的流水就不可能有简单有可靠又风险低的方案,毕竟就算百万级的异常率,每天损失也高达数百万,亿级别的异常率可不是轻易就能做出来的,不要想着有银弹解决所有问题
    qh666
        36
    qh666  
       37 天前
    @sujin190 标题说的 一个商户一秒一千笔收入记录和一千笔支出记录 24x60x60x1000x2 不就是 1.7 亿多么
    luckyrayyy
        37
    luckyrayyy  
       37 天前
    @dapang1221 有的,大主播的退款
    dapang1221
        38
    dapang1221  
       37 天前
    @luckyrayyy 哪种退款呀,打赏充值还是商品退款。一般退款不是支付,是冲正,用户发起退款,平台接受,平台向支付发退款请求,支付平台给这一单直接标记退款不结算就行了,不涉及主播余额变化
    sujin190
        39
    sujin190  
       37 天前
    @qh666 #36 虽然如此,但是有实际工程经验的都知道没有这么算的吧,峰值容量 1000 但是能稳定维持这量级 7 、8 小时都已经牛逼顶天了,实际情况也就 3 、4 小时能有这量级,我们设计不是在这空中阁楼的瞎意淫而是需要充分考虑实际工程情况和使用场景的,不考虑实际工程情况和使用场景的设计必定是不靠谱的,再说按你这每天数百亿上千亿的流水,就在这几句话就讨论出可靠的解决方方案这也不现实啊,楼主估计想问的也不是这使用场景吧
    fengYH8080
        40
    fengYH8080  
       37 天前
    @wxf666 #33 就按你这个说法,你觉得 1.66 亿次的数据库级别的余额变更消耗大点还是一次的汇总消耗大点。再细抠技术细节,到达了这个数量级的系统,已经不是单机能解决的了,需要多机分布式计算。一个人一天都有这么大的量,存储必是要分库分表,汇总时定好时间节点多机后台计算,在具体写入中间汇总表的时候锁一下,把汇总结果和增量未汇总的记录再汇总一下写入内存余额。
    oldking24
        41
    oldking24  
       37 天前
    好像没有看到大家聊到分布式锁的概念 还是因为并发不需要?求解
    peyppicp
        42
    peyppicp  
       37 天前
    高并发入账:做汇总入账,落完流水走人,定期汇总刷到账户中
    高并发下账:做子账户拆分,多个子账户出,会有一些 trade off ,需要取舍
    xuanbg
        43
    xuanbg  
       37 天前
    @crysislinux 有些情况是不需要保证的,负就负了,难道后面就不能冲正了么?
    luckyrayyy
        44
    luckyrayyy  
       36 天前
    @dapang1221 卖货的商品退款,结算和记账是两个事情吧,不涉及主播已结算余额,但是待结算的钱是要扣除的,就有可能有较大并发量的支出。另外平台补贴户之类的也有可能有大量的支出,不过这个比较灵活一般都可以拆成多个。
    wxf666
        45
    wxf666  
       36 天前
    @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
        46
    julyclyde  
       36 天前
    @luckyrayyy 退款又不是实时业务,慢慢退呗
    qq135449773
        47
    qq135449773  
       36 天前
    很有趣的问题,从来没想过这个问题

    如果用队列的话感觉队列的可靠性可能也是一个很大的问题
    wangliran1121
        48
    wangliran1121  
       36 天前
    顺着 @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
        49
    wxf666  
       36 天前
    @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
        50
    sujin190  
       36 天前 via Android
    @wangliran1121 清算对账也不一定要一天一次,如果流水足够高,一般是需要多次清账对账流程才能保证安全,进一步配合不同层级的风控甚至可以进一步依据风控输出决定每个商户在第几次清账后可以支出

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

    再说吧,这都真金白银付钱了,高并发也毕竟天花板就在哪吧,否则都那么高流水了分区后加钱加机器加人都是不值一提的,实在没必要在技术工程上冒这个风险吧,毕竟问问老板他也会说获得久才能赚得多
    wangliran1121
        51
    wangliran1121  
       35 天前
    @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
        52
    wangliran1121  
       35 天前
    @sujin190 是的,redis 会引入更大的系统复杂度和风险,如果业务真这样,其实不需要考虑成本问题,其实金融级别的分布式数据库可以解决这些潜在的事务问题,比如 TiDB 之类的
    wangliran1121
        53
    wangliran1121  
       35 天前
    @wxf666 补充一点,高并发的思路是尽可能少串行化。
    wxf666
        54
    wxf666  
       35 天前
    @wangliran1121 #51

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

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

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

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


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

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

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

    源码发在当时的[帖子]( /1075881#reply68 )里了,可以去测试一下。
    wangliran1121
        55
    wangliran1121  
       35 天前
    @wxf666
    1 、对于这种交易场景,对账是必须存在的过程,这是一种风控手段,T+1 只是举例子,也可以 T+1min ,所以用户看到的余额清算是稍微不准确的,有些延迟的,这点业务上一般可以接受;
    2 、另外,另一种风控要求就是政策和法律,每一笔入账可能都要经过企业内部的风控模型审核完后才能入账,可以是机审,也可以是人审,总之无论政策还是企业都会有这样的要求(换句话说,企业必须要有能力判断一笔账是否异常)
    3 、你担心正确性,一般事务性可以保证,但是应付一些极端问题,你通过对账也能修正回来
    4 、题目意思就是单个商户高并发读写的场景,因此按照你的设计,只能是串行,设计思路可行,但是于题意而言,不太符合场景;
    5 、你测试的只是写请求,但是按照你 45 楼的设计思路,实际上一次入库是需要经历一次完整的读写的,你不读上一笔流水的余额,怎么计算下一笔流水的余额呢?另外,你也不支持并发读,意味着你整套读写过程是串行的,代价十分高昂
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3492 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 11:16 · PVG 19:16 · LAX 03:16 · JFK 06:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.