支付系统 架构模板
代表产品:Stripe、支付宝、微信支付、PayPal、各类「收银台 / 聚合支付」 一句话定位:在不可靠的网络和不可信的世界里,把「钱从 A 到 B」做到一分不差、不重不漏、笔笔可对账可追溯。
1. 一句话定位
支付系统 = 一台「正确性压倒一切」的资金状态机 + 一本永远对得平的账。
它和你熟悉的系统最大的不同:别的系统追求快,它首先追求「对」。 宁可慢、宁可拒绝一笔交易,也绝不能算错一分钱、绝不能扣两次款。它的灵魂不是性能,而是幂等、一致性、可对账这三个词。
2. 业务本质:它在解决什么问题
支付系统是资金流转的可信中介:它站在用户、商户、银行 / 卡组织中间,让一笔钱安全、确定地从付款方到达收款方。
它卖的不是技术,是信任——用户敢把卡号交给它,商户敢等它结款。一次算错账、一次重复扣款,信任就崩了。
钱从哪来:每笔交易的手续费、跨境 / 货币转换费、增值服务(分期、风控、对账报表)。
关键事实:钱不能凭空产生,也不能凭空消失。 这条物理般的守恒律,决定了支付系统几乎所有的架构取舍——它不能像普通系统那样「丢了重算一遍就好」。
3. 核心需求与约束
功能性需求:
- [ ] 发起支付(多渠道:银行卡 / 余额 / 第三方钱包)
- [ ] 退款 / 部分退款
- [ ] 账户与余额(谁有多少钱)
- [ ] 对账(和银行 / 渠道核对每一笔)
- [ ] 清算与结算(把钱真正划给商户)
非功能性需求 / 质量属性(这里和普通系统天差地别):
| 质量属性 | 目标 | 为什么对这类系统重要 |
|---|---|---|
| 正确性 | 一分不差 | 这是底线中的底线,高于一切 |
| 幂等性 | 重复请求绝不重复扣款 | 网络会重试,没有幂等就会多扣钱 |
| 一致性 | 资金强一致 | 余额、账本不能出现「凭空多 / 少」 |
| 可审计 | 每一笔可追溯 | 监管要求、纠纷举证、事后查账 |
| 可用性 | 99.99%+ | 但正确性优先于可用性:拿不准时宁可拒绝 |
关键约束(不可逾越的边界):
- 🔴 资金守恒:任何时刻账必须对得平,不允许中间态把钱「变没」。
- 🔴 外部渠道不可靠且异步:银行可能超时、可能几小时后才回结果,「状态未知」是常态而非异常。
- 🔴 合规是硬约束:PCI-DSS(卡数据)、反洗钱、监管报送,不是「以后再说」。
- 🔴 超时 ≠ 失败:一笔请求超时了,它可能成功了、也可能失败了——这是支付最难的地方。
4. 架构全景图
用户 / 商户
│ 发起支付
▼
┌──────────────────┐
│ 支付网关 / 收银台 │ 接入、验签、令牌化(卡号不落地)
└────────┬─────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 支付编排(状态机引擎)—— 业务核心 │
│ • 创建支付单(幂等键去重) • 风控检查 │
│ • 驱动状态:待支付→处理中→成功/失败/未知 │
│ • 选路由:走哪个渠道 │
└───┬──────────────┬───────────────┬──────────────┬─────────┘
▼ ▼ ▼ ▼
┌────────┐ ┌────────────┐ ┌──────────┐ ┌─────────────────┐
│ 风控 │ │ 渠道适配器 │ │ 账本 │ │ 异步通知 / 回调 │
│ 反欺诈 │ │ (银行/钱包) │ │ (复式记账) │ │ 处理(以查询为准) │
└────────┘ └─────┬──────┘ └────┬─────┘ └─────────────────┘
│ 异步、可能超时 │
▼ ▼
┌──────────┐ ┌──────────────┐
│ 外部银行 │ │ 对账系统 │ 每日和渠道逐笔核对,
│ 卡组织 │───▶│ (兜底真相) │ 差异自动 / 人工处理
└──────────┘ └──────────────┘灵魂部件是账本(Ledger)+ 对账系统:前者用「复式记账」保证任意时刻账都平,后者用「和银行逐笔核对」兜住一切异步与未知。这两样东西的存在,就是为了让「钱」永远算得清。
5. 组件职责
- 支付网关 / 收银台:接入各端、验签、把敏感卡号令牌化(用 token 代替真实卡号,卡号不进入内部系统)。为什么需要:把合规风险和敏感数据挡在最外层。
- 支付编排 / 状态机:整个支付的大脑。用幂等键对请求去重,驱动支付单的状态流转,选渠道路由。为什么需要:支付的本质是一个有明确状态、不可乱跳的状态机。
- 渠道适配器:把内部统一指令翻译成各家银行 / 钱包的协议。为什么需要:屏蔽外部差异;外部调用天然异步、可能超时。
- 账本(Ledger):用复式记账(每笔交易同时记借方和贷方,两边相等)记录资金变动。为什么需要:用「结构」保证资金守恒、可审计、不可篡改(见决策 2)。
- 风控 / 反欺诈:实时判断这笔交易是否可疑。为什么需要:支付是欺诈重灾区。
- 对账系统:每天把自己的流水和银行 / 渠道的流水逐笔核对,找出差异。为什么需要:这是应对「状态未知」的终极兜底——以双方都认的事实为准。
- 异步通知处理:接收渠道回调,但不轻信回调,以己方主动查询为准。
6. 关键数据流
场景一:一次卡支付(核心路径)
1. 用户提交支付 ──▶ 网关:验签、卡号令牌化
2. ──▶ 编排层:用「幂等键」查重
已存在 ──▶ 直接返回上次结果(不重复发起!)
不存在 ──▶ 创建支付单(状态=处理中)
3. ──▶ 风控:放行 / 拦截
4. ──▶ 渠道适配器 ──▶ 外部银行(异步,可能超时)
5. 银行返回成功 ──▶ 账本记一对分录(借:用户 贷:商户),状态=成功
6. ──▶ 异步通知商户;周边系统(积分/发货)订阅事件,最终一致场景二:超时了怎么办?(支付最难的场景)
第 4 步银行迟迟不回 / 超时:
✗ 错误做法:直接判失败 ──▶ 用户可能已被扣款,你却记成失败 → 钱消失
✓ 正确做法:状态置为「未知」,启动【查询补偿】:
隔一会儿主动问银行「这笔到底成没成?」
仍拿不准 ──▶ 留给当天【对账】兜底,以银行最终流水为准这就是为什么支付里**「未知」必须是一等公民的状态**,而不能粗暴归为「失败」。
7. 数据模型与存储选择
核心实体:支付单(带状态机);账本分录(借/贷成对);渠道流水;对账差异。
| 数据 | 存储类型 | 为什么 |
|---|---|---|
| 支付单 / 账户 | 关系型(强事务) | 资金操作要 ACID、强一致 |
| 账本分录 | 追加型 / 不可变日志 | 只增不改不删,保证可审计、不可篡改 |
| 渠道流水、对账 | 关系型 / 列存 | 海量、按日聚合核对 |
| 幂等键 | KV(带唯一约束) | 高速查重,防重复扣款 |
教学点:账本永远只追加、绝不更新或删除(余额是「把所有分录加起来」算出来的,而不是存一个会被覆盖的数字)。这让历史不可篡改、随时可重算、可审计。
8. 关键架构决策与权衡 ⭐
决策 1:如何保证幂等?(支付的生命线)⭐
- 不做幂等:网络重试时,同一笔支付被发起两次 → 重复扣款,灾难。
- 做幂等:每个请求带唯一「幂等键」,服务端用唯一约束保证「同一个键只处理一次」,重复请求直接返回首次结果。
- 取向:必做,没有例外。 幂等是一切「会被重试的写操作」的安全带。
决策 2:账户余额怎么记?「改余额字段」还是「复式记账」?⭐
- 改余额字段(
UPDATE 余额 = 余额 - 100):直观,但并发下易丢更新、无法审计、对不了账、出错难追溯。 - 复式记账(每笔交易记成对的借贷分录,余额由分录累加得出):天然守恒、可审计、不可篡改、能精确还原任意时刻。
- 取向:正经支付一定用复式记账。代价是模型更复杂、写入更多。这是「用数据结构本身保证正确性」的典范。
决策 3:超时 / 失败如何处理?
- 超时就当失败:简单,但可能用户已扣款 → 钱消失,严重事故。
- 引入「未知」状态 + 查询补偿 + 对账兜底:复杂,但不丢钱。
- 取向:必须区分「失败」和「未知」,用主动查询和对账把「未知」收敛到确定。
决策 4:核心强一致,周边最终一致。
- 全链路都强一致:不可能,也没必要(还拖垮性能)。
- 取向:资金核心(扣款、记账)强一致;周边(发短信、加积分、发货通知)走事件驱动、最终一致。把「不能错的」和「晚一点没关系的」分开,是关键判断。
9. 规模化与瓶颈
- 第一个瓶颈:热点账户写入争抢(平台大商户的收款账户被无数笔并发记账)。→ 破解:账户维度的串行化 / 排队、批量合并记账、子账户拆分。
- 第二个瓶颈:对账数据量随交易量爆炸。 → 破解:分库分表 + 离线批处理 + 增量对账。
- 第三个瓶颈:外部渠道限流 / 抖动。 → 破解:多渠道路由 + 降级,单渠道故障自动切换。
- 资金核心不能无脑分片:跨片转账会引出分布式事务,所以分片要顺着「账户」这种天然边界切,避免跨片资金操作。
10. 安全与合规要点
- 🔴 卡数据合规(PCI-DSS):真实卡号绝不落地,用令牌化 / 第三方代收;内部系统只见 token。
- 防重放与验签:所有外部请求 / 回调都要签名校验,防伪造。
- 回调不可信:伪造「支付成功」回调是常见攻击 → 一律以己方主动查询银行的结果为准。
- 风控反欺诈:盗卡、洗钱、套现的实时识别与拦截。
- 审计日志不可删:谁、何时、改了什么,全留痕,满足监管。
- 最小权限:能动钱的接口权限收到最紧,关键操作多重审批。
11. 常见误区 / 反模式
- ❌ 用「读余额 - 改余额 - 写回」更新账户 → ✅ 复式记账 + 不可变分录,杜绝丢更新、可审计。
- ❌ 不做幂等,重试导致重复扣款 → ✅ 幂等键 + 唯一约束,是底线。
- ❌ 把外部回调当可信来源 → ✅ 验签 + 以己方查询为准。
- ❌ 超时直接判失败 → ✅ 引入「未知」状态,查询补偿 + 对账兜底。
- ❌ 敏感卡号明文存储 / 打日志 → ✅ 令牌化,卡号不落地。
- ❌ 追求性能牺牲正确性 → ✅ 支付里正确性绝对优先,慢一点、拒一笔都好过算错。
12. 演进路线:MVP → 成长期 → 成熟期
| 阶段 | 规模量级 | 架构长什么样 | 此时该操心什么 |
|---|---|---|---|
| MVP | 起步 | 接一个成熟第三方支付(自己不碰资金、不存卡号),薄薄一层封装 | 合规与安全外包给专业方,先跑通业务 |
| 成长期 | 多渠道 / 上规模 | 自建支付编排与状态机、多渠道路由、自建账本与每日对账、风控 | 幂等、对账、未知态处理,把「对」做扎实 |
| 成熟期 | 平台级 / 跨境 | 资金清结算、多币种、监管报送、热点账户优化、多活容灾 | 资金安全、合规、容灾、规模化对账 |
13. 可复用要点
- 💡 幂等是一切「会被重试的写操作」的安全带。 不止支付,任何分布式写入都该问:「重复执行一次会不会出事?」
- 💡 用数据结构本身保证正确性,胜过用代码小心翼翼。 复式记账让「账永远平」成为结构属性,而不是靠程序员不犯错。
- 💡 把「未知」当一等公民。 分布式世界里超时 ≠ 失败,承认并主动收敛不确定性,比假装它不存在安全得多。
- 💡 分清「不能错的」和「晚一点没关系的」,核心强一致、周边最终一致,是性能与正确性兼得的关键判断。
- 💡 外部输入永远不可信——回调要验签、要以己方查询为准。
🎯 随堂检验
- A加锁
- B幂等键:同一请求只处理一次
- C多打日志
参考原型与延伸阅读
本模板基于以下真实开源项目与工程博客整理。
🔧 开源原型(可直接读代码):
- tigerbeetle/tigerbeetle — 为金融交易设计的数据库,内置 accounts/transfers 复式记账,体现账本一致性与高性能 OLTP。
- juspay/hyperswitch — 开源、可组合的支付平台(Rust),多渠道路由 / 对账 / 收单连接器,PCI 合规。
📖 工程博客:
- Stripe: Designing robust and predictable APIs with idempotency — 幂等键 + 客户端重试 + 指数退避,支付防重复扣款的奠基文章。
📌 一句话记住支付系统:它不是「一个能扣款的接口」,而是「一台在不确定世界里仍能把账算得分毫不差的状态机」——所有设计都在回答『怎么做到不重、不漏、不错、且笔笔可查』。