支付系统设计
支付系统是金融级严肃场景的代表——钱不能丢、不能多、不能少。设计支付系统意味着在分布式环境下保证精确的余额一致性和事务原子性。
一、核心需求
| 需求 | 说明 |
|---|---|
| 精确性 | 一分钱不能多、一分钱不能少 |
| 幂等性 | 同一笔支付请求重试不能重复扣款 |
| 原子性 | 扣款和入账必须同时成功或同时失败 |
| 可审计 | 每笔交易有完整的流水记录和凭证 |
| 低延迟 | 支付确认在秒级完成 |
| 高可用 | 99.99%+ 可用性,不能因为支付故障影响交易 |
二、幂等性设计
支付系统最核心的要求:同一笔支付请求,重试多少次都只能成功扣款一次。
2.1 幂等键
每笔支付请求携带一个唯一的幂等键(Idempotency Key),由客户端生成:
1 | POST /v1/payments |
2.2 服务端幂等实现
1 | 1. 收到请求 → 检查 Idempotency Key 是否已存在 |
关键注意:如果第 2 步执行到一半服务器宕机,重启后客户端重试(携带相同 Idempotency Key),服务端需要能够恢复并继续之前未完成的交易。这要求支付状态持久化到数据库,而非仅依赖 Redis。
三、分布式事务
3.1 为什么支付需要分布式事务
一次用户支付涉及多个系统的状态变更:
1 | 用户支付 100 元购买商品 |
这四个操作必须在整体上满足原子性——要么全部成功,要么全部不执行。
3.2 方案选型
| 方案 | 适用场景 | 复杂度 |
|---|---|---|
| TCC (Try-Confirm-Cancel) | 强一致性要求 | 高 |
| Saga (补偿事务) | 长事务 | 中 |
| 本地消息表 | 需要异步解耦 | 中 |
| 两阶段提交 (2PC) | 数据库层面 | 低(但性能差) |
支付场景通常用 TCC 模式:
Try 阶段(预留资源):
1 | 用户账户服务:冻结 100 元(余额 500 → 可用 400,冻结 100) |
Confirm 阶段(确认执行):
1 | 用户账户服务:解冻并扣除 100 元(冻结 100 → 余额 400) |
Cancel 阶段(回滚):
1 | 用户账户服务:解冻 100 元(可用 400 → 500) |
3.3 空回滚与悬挂
分布式事务的两个经典异常:
空回滚(Empty Rollback):Cancel 请求先于 Try 请求到达。服务端需要识别出”从未执行过此 Try”并忽略 Cancel。
防悬挂(Prevent Hanging):确认 Try 已超时被 Cancel,但延迟的 Try 请求又到达了。服务端需要识别出”此 Try 已被 Cancel”并拒绝执行。
两者都需要通过记录事务状态来实现:在执行任何操作前先检查事务日志中的 TCC 阶段状态。
四、账户模型
4.1 复式记账
支付系统最基础的账户模型——每笔交易都有借方和贷方,借贷必相等:
1 | 交易 ID: TXN-001 |
4.2 账户表设计
1 | accounts 表: |
余额更新使用乐观锁:
1 | UPDATE accounts |
如果 version 不匹配(被其他事务修改)或余额不足,更新失败,上层重试。
五、风控系统
支付系统的风控需要在 100 毫秒以内给出决策。
5.1 风控检测维度
| 维度 | 规则示例 |
|---|---|
| 用户行为 | 短时间内多次支付、异地登录后支付 |
| 金额异常 | 单笔金额远超历史均值 |
| 设备指纹 | 新设备、模拟器、Root/越狱设备 |
| 网络环境 | IP 黑名单、代理/VPN 检测 |
| 关联风险 | 与已知欺诈账户的设备/IP 重合 |
5.2 架构
1 | 支付请求 |
六、对账系统
对账是支付系统的最后一道防线:每天凌晨银行返回清算文件,与内部交易流水做逐笔比对。
6.1 对账流程
1 | 1. 获取银行清算文件(通常是 CSV/XML) |
七、小结
支付系统的核心原则:一秒的不一致 = 钱的损失。幂等性保证不重复扣款,TCC 分布式事务保证跨服务原子性,复式记账保证每笔交易可追溯,风控系统在 100ms 内拦截欺诈。这些设计模式不仅适用于支付,在任何涉及资金、库存、积分等需要精确计数的系统中都同样适用。