钱包高并发设计:热点账户、幂等扣减与分库分表实践
本文是“钱包系统设计”专题第一篇,聚焦支付、转账、收单等高并发资金扣减场景。目标不是讲一个能跑的 Demo,而是讲清楚生产系统里最容易出事故的几件事:如何防超卖、如何防重复扣款、如何抗住热点账户、如何在分库分表后仍然把账做对。
一、为什么钱包扣减比库存扣减更难
很多人第一次做钱包系统,会把它类比成库存系统:有余额就扣,没有就返回失败。
这个理解只对了一半。
库存超卖,通常是履约问题;钱包超扣,直接是资损问题。库存少发一件货,还能补发、退款、人工介入;钱包一旦多扣、漏记、错记,就会变成真实的资金差错、对账差异、投诉升级,甚至审计问题。
所以钱包扣减的第一原则不是快,而是:
- 不能重复扣;
- 不能扣成负数;
- 不能成功返回但账没落;
- 不能只改余额不记流水;
- 不能局部成功后没人兜底。
换句话说,钱包系统不是一个“余额字段更新器”,而是一个围绕账户、流水、状态机、幂等与补偿构建的资金账务系统。
二、从一次真实故障切入:为什么会“又慢又错”
某交易平台做大促时,支付链路峰值达到 6 万 TPS。用户付款后,钱包系统要同时完成三类账务动作:
- 扣减用户余额;
- 增加商户待结算余额;
- 增加平台手续费账户余额。
事故现象非常典型:
- 支付接口 P99 从 80ms 飙到 7s;
- MySQL 活跃线程暴增,连接池耗尽;
- 平台手续费账户更新严重排队;
- 回调重试导致少量交易重复扣减;
- 流水表写入延迟,账实不一致告警出现。
复盘后,根因不是一个点,而是一整串设计问题叠加:
- 用户余额、商户入账、平台手续费全部放在同一个同步事务中;
- 同一平台账户被高频更新,形成单行热点;
- 幂等只做了 Redis
SETNX,数据库没有唯一约束; - 流水表单表过亿后,索引写放大严重;
- 所有资金路径都要求实时入账,没有区分“必须强一致”和“允许异步汇总”。
这类事故说明,钱包高并发设计不是“给数据库加锁”那么简单,而是要先分清楚三种不同性质的资金动作:
- 用户侧扣减:必须强一致;
- 热点收款账户入账:可以削峰异步;
- 账务审计流水:必须完整、可追溯、可重放。
三、先定义清楚业务模型
在讲架构之前,先把领域模型定义准。钱包系统里至少有四个关键对象。
3.1 账户 Account
账户是资金承载实体,核心字段通常包括:
account_id:账户 ID;owner_id:用户 ID 或商户 ID;account_type:USER、MERCHANT、PLATFORM;available_balance:可用余额;frozen_balance:冻结余额;status:NORMAL、LOCKED、CLOSED;version:乐观锁版本。
3.2 资金流水 AccountFlow
流水是账务审计依据,必须满足“可查、可重放、不可篡改”。一笔业务通常至少会生成一条或多条流水,例如:
- 用户支付扣减流水;
- 商户收款流水;
- 平台手续费流水;
- 补偿冲正流水。
3.3 账务指令 LedgerCommand
业务系统不应该直接“改余额”,而应该提交账务指令,例如:
PAY_DEDUCTTRANSFER_OUTTRANSFER_INFREEZEUNFREEZEFEE_COLLECT
把“业务意图”和“账户更新”解耦,后续才有幂等、审计、补偿、对账的空间。
3.4 业务单号 BizOrder
任何资金动作都必须能回溯到原始业务单号,例如:
- 支付单
pay_order_no - 提现单
withdraw_order_no - 转账单
transfer_order_no
后续所有幂等设计、去重设计、对账设计,都是围绕业务单号展开的。
四、钱包高并发的四个核心矛盾
4.1 既要高并发,又要不超扣
高并发意味着大量请求同时命中同一个服务实例、同一个数据库、甚至同一行记录;不超扣意味着必须对余额校验和扣减做原子控制。
4.2 既要防重复,又不能把吞吐做没
支付通知重试、MQ 重投、网关重试、客户端超时补发,都会造成重复请求。单纯加全局锁会很安全,但吞吐会急剧下降。
4.3 既要分库分表,又要维持账务闭环
账户表可以分片,流水表也可以分片,但一笔交易可能涉及多个账户、多个分片、多个服务。分了之后,如何保证账平,是更大的难题。
4.4 既要热点削峰,又要最终账准
平台手续费账户、大商户收款账户天然是热点。直接同步写 DB,一定会卡;全走缓存异步,又会有一致性风险。工程上必须分层处理。
五、总体架构:把“强一致路径”和“削峰路径”拆开
生产级钱包系统,建议采用“双路径架构”:
- 路径 A:用户余额强一致扣减路径;
- 路径 B:热点入账异步汇总路径。


74

被折叠的 条评论
为什么被折叠?



