# 支付系统 架构模板 > **代表产品**: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. 可复用要点 - 💡 **幂等是一切「会被重试的写操作」的安全带。** 不止支付,任何分布式写入都该问:「重复执行一次会不会出事?」 - 💡 **用数据结构本身保证正确性,胜过用代码小心翼翼。** 复式记账让「账永远平」成为结构属性,而不是靠程序员不犯错。 - 💡 **把「未知」当一等公民。** 分布式世界里超时 ≠ 失败,承认并主动收敛不确定性,比假装它不存在安全得多。 - 💡 **分清「不能错的」和「晚一点没关系的」**,核心强一致、周边最终一致,是性能与正确性兼得的关键判断。 - 💡 **外部输入永远不可信**——回调要验签、要以己方查询为准。 ## 🎯 随堂检验 --- ## 参考原型与延伸阅读 > 本模板基于以下**真实开源项目**与**工程博客**整理。 **🔧 开源原型(可直接读代码):** - [tigerbeetle/tigerbeetle](https://github.com/tigerbeetle/tigerbeetle) — 为金融交易设计的数据库,内置 accounts/transfers 复式记账,体现账本一致性与高性能 OLTP。 - [juspay/hyperswitch](https://github.com/juspay/hyperswitch) — 开源、可组合的支付平台(Rust),多渠道路由 / 对账 / 收单连接器,PCI 合规。 **📖 工程博客:** - [Stripe: Designing robust and predictable APIs with idempotency](https://stripe.com/blog/idempotency) — 幂等键 + 客户端重试 + 指数退避,支付防重复扣款的奠基文章。 --- > 📌 一句话记住支付系统:**它不是「一个能扣款的接口」,而是「一台在不确定世界里仍能把账算得分毫不差的状态机」——所有设计都在回答『怎么做到不重、不漏、不错、且笔笔可查』。**