Skip to content

第 26 章:支付系统

引言

本章设计一个支撑现代电子商务支付系统

支付系统负责结算金融交易,转移货币价值。


第 1 步:理解问题并确定设计范围

  • 候选人:要构建哪种支付系统?
  • 面试官:类似 Amazon.com 的电子商务系统支付后端,处理所有与资金流转有关的事务。
  • 候选人:支持哪些支付方式,例如信用卡、PayPal、银行卡?
  • 面试官:现实中应支持所有这些方式;面试中只考虑信用卡支付。
  • 候选人:由我们自行处理信用卡交易吗?
  • 面试官:不,使用 Stripe、Braintree、Square 等第三方提供商。
  • 候选人:系统会存储信用卡数据吗?
  • 面试官:出于合规原因,系统不直接存储信用卡数据,而依赖第三方支付处理机构。
  • 候选人:应用面向全球吗?需要支持多种货币和国际支付吗?
  • 面试官:应用面向全球,但面试中假设只使用一种货币。
  • 候选人:每天需要处理多少笔支付交易?
  • 面试官:100 万笔。
  • 候选人:需要支持向收款方按月付款等付款流程吗?
  • 面试官:需要。
  • 候选人:还有哪些方面需要注意?
  • 面试官:需要支持对账,以修复与内部和外部系统通信过程中出现的不一致。

功能需求

  • 收款流程:支付系统代表商家从客户处收款。
  • 付款流程:支付系统向世界各地的卖家付款。

非功能需求

  • 可靠性与容错能力:必须谨慎处理支付失败。
  • 建立内部系统与外部系统之间的对账机制。

粗略估算

系统每天需要处理 100 万笔交易,约为每秒 10 笔。

对数据库而言,这不算高吞吐量,因此不是本次面试的重点。


第 2 步:提出高层设计并取得共识

从高层看,资金流转涉及三个参与方:

<img src="/images/chapter-26/high-level-flow.png" alt="高层资金流程" width="500" />

收款流程

收款流程的高层概览:

<img src="/images/chapter-26/payin-flow-high-level.png" alt="收款流程高层设计" width="500" />
  • 支付服务:接收支付事件并协调支付流程。通常还会通过第三方机构检查反洗钱(AML)违规或犯罪活动风险。
  • 支付执行器:通过支付服务提供商(PSP)执行单个支付订单。一个支付事件可包含多个支付订单。
  • 支付服务提供商(PSP):在账户间转移资金,例如从买家的信用卡账户转至电商网站的银行账户。
  • 卡组织:处理信用卡业务的机构,例如 Visa、MasterCard。
  • 分类账:保存所有支付交易的财务记录。
  • 钱包:保存所有商家的账户余额。

收款流程示例:

  • 用户点击“下单”,支付事件发送到支付服务。
  • 支付服务将事件存入数据库。
  • 支付服务针对事件中的所有支付订单调用支付执行器。
  • 支付执行器将支付订单存入数据库。
  • 支付执行器调用外部 PSP 处理信用卡支付。
  • 支付执行器完成支付处理后,支付服务更新钱包,记录卖家收到的金额。
  • 钱包服务将更新后的余额存入数据库。
  • 支付服务调用分类账,记录所有资金流转。

支付服务 API

POST /v1/payments
{
  "buyer_info": {...},
  "checkout_id": "some_id",
  "credit_card_info": {...},
  "payment_orders": [{...}, {...}, {...}]
}

payment_order 示例:

{
  "seller_account": "SELLER_IBAN",
  "amount": "3.15",
  "currency": "USD",
  "payment_order_id": "globally_unique_payment_id"
}

注意事项:

  • payment_order_id 会传递给 PSP,用于去重支付,即作为幂等键。
  • amount 字段使用 string,因为 double 不适合表示货币金额。
GET /v1/payments/{:id}

此接口根据 payment_order_id 返回单笔支付的执行状态。

支付服务数据模型

需要维护 payment_eventspayment_orders 两张表。

对支付业务来说,性能通常不是首要因素,强一致性才是。

选择数据库还应考虑:

  • 是否有充足的数据库管理员人才可供招聘。
  • 其他大型金融机构是否已有可靠的使用记录。
  • 配套工具是否完善。
  • 传统 SQL 数据库相对 NoSQL/NewSQL 所提供的 ACID 保证。

payment_events 表包含:

  • checkout_id:字符串,主键。
  • buyer_info:字符串(作者个人认为,外键关联另一张表可能更合适)。
  • seller_info:字符串(同上)。
  • credit_card_info:取决于银行卡服务商。
  • is_payment_done:布尔值。

payment_orders 表包含:

  • payment_order_id:字符串,主键。
  • buyer_account:字符串。
  • amount:字符串。
  • currency:字符串。
  • checkout_id:字符串,外键。
  • payment_order_status:枚举(NOT_STARTEDEXECUTINGSUCCESSFAILED)。
  • ledger_updated:布尔值。
  • wallet_updated:布尔值。

注意事项:

  • 一个支付事件可以关联多个支付订单。
  • 收款流程不需要 seller_info,只有付款流程需要。
  • 调用相应服务记录支付结果后,更新 ledger_updatedwallet_updated
  • 后台任务管理支付状态转换:它检查进行中的支付是否有更新,并在支付未于合理时间内完成时触发告警。

复式记账系统

复式记账是支付系统的核心机制。每笔资金操作都记录在两个账户中:一个账户余额增加(贷记),另一个减少(借记)。

账户借方贷方
买家$1
卖家$1

所有交易分录的总和始终为零。该机制使系统内的每笔资金流转都可以端到端追踪。

托管支付页面

为了避免自行存储信用卡信息并承担繁重的合规义务,多数公司倾向于使用 PSP 提供的组件,由 PSP 存储并处理信用卡支付信息:

<img src="/images/chapter-26/hosted-payment-page.png" alt="托管支付页面" width="500" />

付款流程

付款流程的组件与收款流程非常相似。

主要区别:

  • 资金从电商网站的银行账户转至商家的银行账户。
  • 可以使用 Tipalti 等第三方应付账款服务商。
  • 付款同样涉及大量记账和监管要求。

第 3 步:深入设计

本节重点讨论如何让系统更快、更稳健、更安全。

集成 PSP

如果系统能直接连接银行或卡组织,则无须 PSP 也可以完成支付。但这种连接非常少见,通常只有能承担相应投资的大型公司才会采用。

沿用常规方式时,有两种集成 PSP 的方法:

  • 如果支付系统可以收集付款信息,通过 API 集成。
  • 通过托管支付页面集成,避免直接处理付款信息带来的监管要求。

托管支付页面的工作流程如下:

<img src="/images/chapter-26/hosted-payment-page-workflow.png" alt="托管支付页面流程" width="500" />
  • 用户在浏览器中点击“结账”按钮。
  • 客户端将支付订单信息发送给支付服务。
  • 支付服务收到订单信息后,向 PSP 发送支付登记请求。
  • PSP 收到货币、金额、有效期等支付信息,以及一个用于幂等处理的 UUID,通常是支付订单的 UUID。
  • PSP 返回唯一标识本次支付登记的令牌,支付服务将令牌存入数据库。
  • 令牌存储后,系统向用户展示由 PSP 托管的支付页面,并使用令牌及成功、失败时的重定向 URL 初始化页面。
  • 用户在 PSP 页面填写支付信息,PSP 处理支付并返回状态。
  • 用户被重定向到返回地址,例如 https://your-company.com/?tokenID=JIOUIQ123NSF&payResult=X324FSa
  • PSP 通过 webhook 异步调用支付服务,通知后端支付结果。
  • 支付服务根据收到的 webhook 记录支付结果。

对账

上一节描述的是支付顺利完成的路径。失败和异常路径由后台对账流程发现并处理。

PSP 每晚发送结算文件,系统用它对比外部系统与内部系统的状态。

<img src="/images/chapter-26/settlement-report.png" alt="结算报表" width="500" />

这一流程也可用于发现分类账和钱包服务等内部系统之间的不一致。

财务团队会处理不匹配的记录,包括:

  • 可归类且可按标准流程调整的已知差异。
  • 可归类但无法自动处理的差异,由财务团队手工调整。
  • 无法归类的差异,由财务团队手工调查并调整。

处理支付延迟

支付通常几秒钟即可完成,但有时可能需要数小时。

原因可能包括:

  • 支付被标为高风险,需要人工审核。
  • 信用卡需要额外保护,例如 3D Secure 身份验证,需要持卡人补充信息。

可通过以下方式处理:

  • 等待 PSP 在支付完成后发送 webhook;如果 PSP 不提供 webhook,则轮询其 API。
  • 向用户显示“处理中”状态,并提供可查看支付进度的页面。支付完成后也可以发送电子邮件通知。

内部服务间通信

服务之间的通信模式有两种:同步和异步。

同步通信(如 HTTP)适合小规模系统,但随规模增大会暴露问题:

  • 性能较低:调用链涉及的服务越多,请求响应周期越长。
  • 故障隔离差:PSP 或其他服务故障时,用户可能收不到响应。
  • 耦合紧密:发送方必须了解接收方。
  • 难以扩展:缺少缓冲机制,不易承受突然增长的流量。

异步通信又可分为两类。

单接收方:多个接收方订阅同一主题,但每条消息只处理一次:

<img src="/images/chapter-26/single-receiver.png" alt="单接收方" width="500" />

多接收方:多个接收方订阅同一主题,每条消息会转发给所有接收方:

<img src="/images/chapter-26/multiple-receiver.png" alt="多接收方" width="500" />

后一种模式适合支付系统,因为一次支付可能触发由不同服务处理的多个后续操作。

简而言之,同步通信较简单,但限制服务的自主性。异步通信以简单性和一致性为代价,换取可扩展性与韧性。

处理支付失败

所有支付系统都必须处理失败。可以采用以下机制:

  • 跟踪支付状态:失败时,根据支付状态决定重试还是退款。

  • 重试队列:将需要重试的支付发布到重试队列。

  • 死信队列:将最终失败的支付放入死信队列,以便调试和检查。

    支付失败处理

恰好一次处理

必须确保每笔支付只被处理一次,以免重复向客户扣款。

当一项操作同时满足“至少一次”和“至多一次”时,就是恰好执行一次。

为保证“至少一次”,可以使用重试机制:

<img src="/images/chapter-26/retry-mechanism.png" alt="重试机制" width="500" />

常见的重试间隔策略:

  • 立即重试:失败后客户端立即再次发送请求。
  • 固定间隔:每次重试前等待固定时长。
  • 递增间隔:逐次增加等待时间。
  • 指数退避:每次重试将等待时间加倍。
  • 取消:错误无法恢复或达到重试次数上限时,客户端取消请求。

一般可默认采用指数退避。较好的做法是由服务器通过 Retry-After 响应头指定重试时间。

重试的问题在于服务器可能重复处理支付:

  • 客户端两次点击“支付”按钮,导致重复扣款。
  • PSP 已成功处理支付,但下游服务(分类账、钱包)尚未完成处理。重试又让 PSP 处理同一笔支付。

为避免重复支付,需要幂等机制:同一操作即使应用多次,也只产生一次处理结果。

从 API 看,客户端可以多次调用而得到相同结果。通常使用请求中的特殊头字段(例如 idempotency-key)管理幂等性,其值一般为 UUID。

<img src="/images/chapter-26/idempotency-example.png" alt="幂等示例" width="500" />

可以利用数据库的唯一键约束实现幂等性:

  • 服务器尝试向数据库插入新行。
  • 如果违反唯一键约束,插入失败。
  • 服务器识别该错误,改为向客户端返回已有对象。

PSP 侧也会使用前文提及的一次性随机数(nonce)实现幂等性,避免处理两次具有相同 nonce 的支付。

一致性

支付生命周期中涉及多个有状态服务:PSP、分类账、钱包和支付服务。

任意两个服务之间的通信都可能失败。通过实现恰好一次处理与对账,可以让各服务的数据最终一致。

如果使用复制,还需处理复制延迟,因为用户可能从主数据库与副本数据库读到不一致的数据。

一种缓解办法是所有读写都使用主数据库,副本仅用于冗余与故障切换。也可以利用 Paxos 或 Raft 等共识算法保持副本同步,或者采用 YugabyteDB、CockroachDB 等基于共识的分布式数据库。

支付安全

可采用以下机制保护支付:

  • 请求和响应被窃听:使用 HTTPS 保护所有通信。
  • 数据被篡改:实施加密与完整性监测。
  • 中间人攻击:使用 SSL 和证书固定。
  • 数据丢失:跨区域复制数据并生成数据快照。
  • DDoS 攻击:实施限流和防火墙防护。
  • 银行卡信息被盗:使用令牌,避免在系统中存储真实卡片信息。
  • PCI 合规:遵守针对处理品牌信用卡的机构制定的安全标准。
  • 欺诈:使用地址验证、卡片验证码(CVV)、用户行为分析等方法。

第 4 步:总结

其他可讨论的话题:

  • 监控与告警。
  • 调试工具:需要能方便地查明支付失败原因的工具。
  • 货币兑换:设计国际支付系统时十分重要。
  • 地域差异:不同地区可能采用不同支付方式。
  • 现金支付:在印度、巴西等地十分常见。
  • 集成 Google Pay 和 Apple Pay。