18. Design Payment System
In this chapter, we design a payment system. E-commerce has exploded in popularity across the world in recent years. What makes every transaction possible is a payment system running behind the scenes. A reliable, scalable, and flexible payment system is essential.
在本章中,我们将设计一个支付系统。近年来,电子商务在全球范围内蓬勃发展。每笔交易的实现都离不开一个在幕后运行的支付系统。一个可靠、可扩展且灵活的支付系统至关重要。
Reference: https://bytebytego.com/courses/system-design-interview/payment-system
Step 1 - Understand the Problem and Establish Design Scope
1.1 Functional requirements
- Handle credit card payments via third-party processors (e.g., Stripe), without storing sensitive card data
- 通过第三方支付处理器 (如 Stripe )处理信用卡支付,不存储敏感卡片信 息
- Support both customer payments (pay-in) and seller payouts (pay-out)
- 支持顾客付款和卖家结算两个方向的资金流转
- Designed for global use, but assume a single currency for now
- 面向全球用户,但当前仅考虑单一货币
- Process around 1 million transactions per day
- 支持每日约 100 万笔交易的处理能力
- Integrate with internal and external systems, and support reconciliation for consistency
- 与内部系统 (如会计、分析 )和外部服务集成,并支持对账以保证一致性
1.2 Non-functional requirements
- Reliability and fault tolerance. Failed payments need to be carefully handled.
- 可靠性和容错性。付款失败需要谨慎处理。
- A reconciliation process between internal services (payment systems, accounting systems) and external services (payment service providers) is required. The process asynchronously verifies that the payment information across these systems is consistent.
- 需要在内部服务 (支付系统、会计系统 )和外部服务 (支付服务提供商 )之间建立对账流程。该流程异步验证这些系统之间的支付信息是否一致。
1.3 Back-of-the-envelope estimation 粗略估计
The system needs to process 1 million transactions per day, which is 1,000,000 transactions / 10^5 seconds = 10 transactions per second (TPS). 10 TPS is not a big number for a typical database, which means the focus of this system design interview is on how to correctly handle payment transactions, rather than aiming for high throughput.
该系统每天需要处理 100 万笔交易,即 1,000,000 笔交易 / 10^5 秒 = 每秒 10 笔交易 (TPS)。对于典型的数据库来说,10 TPS 并不是一个很大的数字,这意味着本次系统设计面试的重点是如何正确处理支付交易,而不是追求高吞吐量。
Step 2 - Propose High-Level Design and Get Buy-In
Take the e-commerce site, Amazon, as an example. After a buyer places an order, the money flows into Amazon’s bank account, which is the pay-in flow. Although the money is in Amazon's bank account, Amazon does not own all of the money. The seller owns a substantial part of it and Amazon only works as the money custodian for a fee. Later, when the products are delivered and money is released, the balance after fees then flows from Amazon’s bank account to the seller's bank account. This is the pay-out flow. The simplified pay-in and pay-out flows are shown in Figure 1.
以电商网站亚马逊为例。买家下单后,款项会进入亚马逊的银行账户,这就是入账流程。虽然款项在亚马逊的银行账户中,但亚马逊并非所有款项的所有者。卖家拥有其中很大一部分,亚马逊仅作为资金托管人,并收取一定费用。之后,当商品发货并款项到账后,扣除费用后的余额会从亚马逊的银行账户转入卖家的银行账户,这就是出账流程。简化的入账和出账流程如图 1 所示。
2.1 Pay-in flow 入账流程
- Payment service 支付服务
- The payment service accepts payment events from users and coordinates the payment process. The first thing it usually does is a risk check, assessing for compliance with regulations such as AML/CFT [2], and for evidence of criminal activity such as money laundering or financing of terrorism. The payment service only processes payments that pass this risk check. Usually, the risk check service uses a third-party provider because it is very complicated and highly specialized.
- 支付服务接受用户的付款事件并协调支付流程。它通常首先进行风险检查,评估是否符合反洗钱/反恐怖融资[2]等法规,并查找洗钱或恐怖主义融资等犯罪活动的证据。支付服务只处理通过此风险检查的付款。由于风险检查服务非常复杂且高度专业化,通常会使用第三方服务提供商。
- Payment executor 付款执行人
- The payment executor executes a single payment order via a Payment Service Provider (PSP). A payment event may contain several payment orders.
- 支付执行器通过支付服务提供商 (PSP )执行单笔支付订单。一个支付事件可能包含多笔支付订单。
- Payment Service Provider (PSP)
- A PSP moves money from account A to account B. In this simplified example, the PSP moves the money out of the buyer’s credit card account.
- PSP 将资金从账户 A 转移到账户 B。在这个简化的例子中,PSP 将资金从买家的信用卡账户中转出。
- Card schemes
- Card schemes are the organizations that process credit card operations. Well known card schemes are Visa, MasterCard, Discovery, etc. The card scheme ecosystem is very complex [3].
- 信用卡组织是处理信用卡业务的组织。知名的信用卡组织有 Visa、MasterCard、Discovery 等。信用卡组织生态系统非常复杂 [3]。
- Ledger 账本
- The ledger keeps a financial record of the payment transaction. For example, when a user pays the seller $1, we record it as debit $1 from a user and credit $1 to the seller. The ledger system is very important in post-payment analysis, such as calculating the total revenue of the e-commerce website or forecasting future revenue.
- 账本保存支付交易的财务记录。例如,当用户向卖家支付 1 美元时,我们会将其记录为用户借方 1 美元,卖家贷方 1 美元。账本系统在支付后分析中非常重要,例如计算电商网站的总收入或预测未来收入。
- Wallet 钱包
- The wallet keeps the account balance of the merchant. It may also record how much a given user has paid in total.
- 钱包保存着商家的账户余额。它也可能记录特定用户总共支付了多少钱。
As shown in Figure 2, a typical pay-in flow works like this:
- 1.When a user clicks the “place order” button, a payment event is generated and sent to the payment service.
- 当用户点击“下订单”按钮时,就会生成支 付事件并发送到支付服务。
- 2.The payment service stores the payment event in the DB.
- 支付服务将支付事件存储在数据库中。
- 3.Sometimes, a single payment event may contain several payment orders. For example, you may select products from multiple sellers in a single checkout process. If the e-commerce website splits the checkout into multiple payment orders, the payment service calls the payment executor for each payment order.
- 有时,单个支付事件可能包含多个支付订单。例如,您可能在一次结账流程中选择了来自多个卖家的商品。如果电商网站将结账拆分为多个支付订单,则支付服务会为每个支付订单调用支付执行器。
- 4.The payment executor stores the payment order in the DB.
- 支付执行器将支付订单存储在 DB 中。
- 5.The payment executor calls an external PSP to process the credit card payment.
- 付款执行器调用外部 PSP 来处理信用卡付款。
- 6.After the payment executor has successfully processed the payment, the payment service updates the wallet to record how much money a given seller has.
- 付款执行器成功处理付款后,支付服务会更新钱包以记录特定卖家有多少钱。
- 7.The wallet server stores the updated balance information in the DB.
- 钱包服务将更新后的余额信息存储在数据库中。
- 8.After the wallet service has successfully updated the seller’s balance information, the payment service calls the ledger to update it.
- 钱包服务成功更新卖家余额信息后,支付服务会调用分类账进行更新。
- 9.The ledger service appends the new ledger information to DB.
- 记账服务将新的分类帐信息附加到数据库。
2.2 APIs for payment service
We use the RESTful API design convention for the payment service.
2.2.1 POST /v1/payments
This endpoint executes a payment event. As mentioned above, a single payment event may contain multiple payment orders. 此端点执行支付事件。如上所述,单个支付事件可能包含多个支付订单。
{
"buyer_info": {
"name": "Alice Zhang", // 买家姓名 (由前端填写 )
"email": "alice@example.com", // 买家邮箱 (由前端填写 )
"phone": "+1-415-555-1234", // 买家手机号 (由前端填写 )
"address": {
// 买家收货地址 (由前端填写或用户配置 )
"line1": "123 Market Street", // 地址第一行
"city": "San Francisco", // 城市
"state": "CA", // 州/省
"postal_code": "94103", // 邮政编码
"country": "US" // 国家 (ISO 国家码 )
}
},
"checkout_id": "chk_20250705_000123", // 结账单号 (由 Checkout 服务生成,通常在创建购物车结账页时生成 )
"credit_card_info": {
"token": "tok_abc123xyz456", // 支付 token (由前端通过 PSP SDK 获取,比如 Stripe Elements )
"provider": "stripe", // 支付服务商 (由前端或支付配置中心决定 )
"last4": "1111", // 卡号后四位 (由 PSP 返回,前端或支付服务回填 )
"brand": "Visa", // 卡品牌 (由 PSP 返回 )
"expiry_month": "12", // 有效期月份 (由前端填写 )
"expiry_year": "2027" // 有效期年份 (由前端填写 )
},
"payment_orders": [
{
"seller_account": "seller_001", // 卖家账户 (由订单服务 Order Service 决定 )
"amount": "49.99", // 金额 (由订单服务计算 )
"currency": "USD", // 币种 (由商城或国际化服务设置 )ISO 4217
"payment_order_id": "po_20250705_0001" // 支付订单号 (由 Payment Service 生成 )
},
{
"seller_account": "seller_002", // 第二位卖家 (由订单服务识别并拆单 )
"amount": "30.00",
"currency": "USD",
"payment_order_id": "po_20250705_0002" // globally unique ID (idempotency key) 全局唯一幂等键
}
]
}
- the
payment_order_id
is globally unique. When the payment executor sends a payment request to a third-party PSP, thepayment_order_id
is used by the PSP as the deduplication ID, also called the idempotency key.- 全局唯一键。当支付执行方向第三方支付服务提供商 (PSP) 发送支付请求时,payment_order_id 会被 PSP 作为去重 ID,也称为幂等键
- the data type of the “amount” field is “string,” rather than “double”. Double is not a good choice because:
- Different protocols, software, and hardware may support different numeric precisions in serialization and deserialization. This difference might cause unintended rounding errors.
- 不同的协议、软件和硬件在序列化和反序列化过程中可能支持不同的数值精度。这种差异可能会导致意外的舍入误差。
- The number could be extremely big (for example, Japan’s GDP is around 5x1014 yen for the calendar year 2020), or extremely small (for example, a satoshi of Bitcoin is 10-8).
- It is recommended to keep numbers in string format during transmission and storage. They are only parsed to numbers when used for display or calculation.
- 建议在传输和存储过程中将数字保留为字符串格式。只有在显示或计算时才会将其解析为数字。
2.2.2 GET /v1/payments/{:id}
- This endpoint returns the execution status of a single payment order based on
payment_order_id
.- 该接口根据
payment_order_id
返回单个支付订单的执行状态。
- 该接口根据
- The payment API mentioned above is similar to the API of some well-known PSPs. If you are interested in a more comprehensive view of payment APIs, check out Stripe’s API documentation https://stripe.com/docs/api.
2.3 The data model for payment service 支付服务的数据模型
We need two tables for the payment service: payment event and payment order. When we select a storage solution for a payment system, performance is usually not the most important factor. Instead, we focus on the following:
- Proven stability. Whether the storage system has been used by other big financial firms for many years (for example more than 5 years) with positive feedback. 稳定性已得到验证。该存储系统是否已被其他大型金融机构使用多年 (例如超过 5 年 ),并获得良好的反馈。
- The richness of supporting tools, such as monitoring and investigation tools. 支持工具的丰富性,例如监控和调查工具。
Usually, we prefer a traditional relational DB with ACID transaction support over NoSQL/NewSQL.
2.3.1 Payment Event 表结构
CREATE TABLE payment_event (
checkout_id VARCHAR(64) PRIMARY KEY COMMENT '结账 ID,唯一标识一次支付事件',
buyer_info TEXT COMMENT '买家信息 (可以是 JSON 字符串 )',
seller_info TEXT COMMENT '卖家信息 (可以是 JSON 字符串或多个卖家标识 )',
credit_card_info TEXT COMMENT '信用卡信息 (根据支付服务商格式存储 )',
is_payment_done BOOLEAN DEFAULT FALSE COMMENT '支付是否完成标志位'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付事件主表';
2.3.2 Payment Order 表结构
CREATE TABLE payment_order (
payment_order_id VARCHAR(64) PRIMARY KEY COMMENT '支付子订单 ID',
buyer_account VARCHAR(64) COMMENT '付款用户账户',
amount DECIMAL(18, 2) COMMENT '支付金额',
currency VARCHAR(10) COMMENT '货币类型 (如 USD、CNY )',
checkout_id VARCHAR(64) COMMENT '关联的结账事件 ID',
payment_order_status VARCHAR(32) COMMENT '支付订单状态 (如 NOT_STARTED, EXECUTING, SUCCESS, FAILED',
ledger_updated BOOLEAN DEFAULT FALSE COMMENT '账本是否已更新',
wallet_updated BOOLEAN DEFAULT FALSE COMMENT '钱包是否已更新',
CONSTRAINT fk_checkout_id FOREIGN KEY (checkout_id)
REFERENCES payment_event(checkout_id)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付子订单表';
- The
checkout_id
is the foreign key. A single checkout creates a payment event that may contain several payment orders.checkout_id
是外键。单次结账会创建一个付款事件,该事件可能包含多个付款订单。
- When we call a third-party PSP to deduct money from the buyer's credit card, the money is not directly transferred to the seller. Instead, the money is transferred to the e-commerce website’s bank account. This process is called pay-in. When the pay-out condition is satisfied, such as when the products are delivered, the seller initiates a pay-out. Only then is the money transferred from the e-commerce website’s bank account to the seller's bank account. Therefore, during the pay-in flow, we only need the buyer’s card information, not the seller’s bank account information.
- 当我们调用第三方支付服务提供商 (PSP) 从买家信用卡扣款时,款项不会直接转给卖家,而是转入电商网站的银行账户。这个过程称 为“到账”。当满足出账条件 (例如商品送达 )时,卖家才会发起出账。只有此时,款项才会从电商网站的银行账户转入卖家的银行账户。因此,在到账流程中,我们只需要买家的卡信息,而无需卖家的银行账户信息。
2.3.3 payment_order_status
状态机
- 1.初始化
- 默认状态为
NOT_STARTED
- 这时支付订单还未发送出去
- 默认状态为
- 2.发送支付请求
- 当支付服务将订单发送到 Payment Executor 后,状态更新为
EXECUTING
- 当支付服务将订单发送到 Payment Executor 后,状态更新为
- 3.支付执行完成
- 如果执行器返回成功,状态更新为
SUCCESS
- 如果执行器返回失败,状态更新为
FAILED
- 如果执行器返回成功,状态更新为
- 4.支付成功后的处理
- 当状态变为 SUCCESS,支付服务会调用 钱包服务 (Wallet Service) 更新卖家的余额
- 钱包更新成功后,字段
wallet_updated
会被置为 TRUE - 为简化设计,假设钱包更新操作 永远成功
- 5.账本更新
- 在钱包更新成功后,支付服务会进一步调用账本服务 (Ledger Service ),将交易信息写入账本数据库,并将 ledger_updated 字段设置为 TRUE。
- 6.整体支付完成检查
- 当某个结账单号 (
checkout_id
)下的所有支付订单都成功执行并完成后续处理后,支付服务会将 payment_event 表中的is_payment_done
字段设置为TRUE
,表示该笔支付已完全完成。
- 当某个结账单号 (
- 7.异常监控机制
- 系统中通常会部署一个定时任务 (Scheduled Job ),定期扫描仍处于进行中 (如 NOT_STARTED 或 EXECUTING )的支付订单。如果发现某笔订单在设定时间内 (如 5 分钟 )未完成处理,则会触发告警,提示工程师及时排查,确保支付流程的稳定性和可靠性。
状态 | 含义 |
---|---|
NOT_STARTED | 初始状态,表示支付服务还未将该订单发送给执行器 (Payment Executor ) |
EXECUTING | 支付执行中,表示支付服务已将该订单发送给执行器,正在进行支付处理 |
SUCCESS | 支付成功,支付执行器返回成功,表示该笔款项已经处理完成 |
FAILED | 支付失败,执行器返回失败,例如余额不足、卡信息无效等问题 |
2.4 Double-entry Ledger System 双重记账系统
在账本系统中有一个非常关键的设计原则——双重记账原则 (double-entry principle,也叫做复式记账 )。这个原则是任何支付系统实现精确记账的基础。
双重记账系统的核心思想是:每一笔支付交易都需要记录成两条账本记录,一个账户是借记 (Debit ),另一个账户是贷记 (Credit ),金额相同。
例如,当买家向卖家支付 1 美元时
- 在账本中,买家账户被借记 $1
- 同时,卖家账户被贷记 $1
2.5 Hosted payment page 托管支付页
托管支付页由 PSP 托管,客户的信用卡信息直接输入给 PSP,而不是我们自己的支付服务系统,这样可以极大地简化系统合规要求并降低风险
大多数公司不愿意在自己的系统中直接存储信用卡信息,因为一旦存储,就必须遵守一系列严格的合规标准,例如美国的 PCI DSS (支付卡行业数据安全标准 ),这些要求既复杂又成本高昂。
为了规避处理信用卡信息带来的合规风险,企业通常会采用由 支付服务提供商 (PSP ) 提供的托管支付页面 (Hosted Payment Page )。这种支付页面通常有以下两种形式:
- 在网页端,是通过嵌入的 widget 或 iframe 实现
- 在移动端,可能是支付 SDK 提供的一个预置支付页面
例如,PayPal 的集成就是一个典型例子,用户在结账流程中会跳转到 PayPal 的托管支付页完成付款。
2.6 Pay-out flow 出账流程
出账流程的系统组件与入账流程 (Pay-in flow )非常相似,但有一个关键区别:
入账流程是通过 PSP (支付服务提供商 )将买家的信用卡资金转入电商平台的银行账户。而出账流程则是通过第三方出账服务商,将钱从电商平台的银行账户转出,打给卖家的银行 账户。
在实际应用中,电商平台通常会使用像 Tipalti 这样的第三方应付服务商 (Accounts Payable Providers )来处理出账操作。
因为出账涉及资金从企业流向个人或卖家账户,它同样面临严格的账务处理和合规监管要求,例如税务申报、付款报表、反洗钱 (AML )检查等。这使得出账流程在技术实现和合规层面都不容忽视。
Step 3 - Design Deep Dive
在本节中,我们的重点是如何让支付系统变得更快、更健壮、更安全。在分布式系统中,错误和故障不仅是可能的,而且是常见的现象。例如:
- 如果用户连续点击了多次“支付”按钮,会不会被重复扣款?what happens if a customer pressed the “pay” button multiple times?
- 网络不稳定时引发的支付失败,我们该如何处理? How do we handle payment failures caused by poor network connections?
- 系统内部各个服务之间如何协作,确保支付可靠完成?
本节将深入探讨几个关键主题
- PSP 接入 (PSP integration )
- 对账处理 (Reconciliation )
- 支付延迟处理 (Handling payment processing delays )
- 内部服务通信 (Communication among internal services )
- 支付失败处理 (Handling failed payments )
- 精准一次性处理 (Exact-once delivery )
- 一致性保障 (Consistency )
- 安全性设计 (Security )
3.1 PSP integration
从理论 上讲,如果支付系统能够直接接入银行或卡组织 (例如 Visa、MasterCard ),就可以绕过 PSP 直接完成支付。但这种方式非常少见,通常只适用于业务规模极大、有能力自行承担合规和集成成本的大型公司。
对大多数公司来说,支付系统会通过以下两种方式接入 PSP
- 方式一: API 集成 (适合可自行处理敏感数据的公司)
- 公司自身开发支付页面,收集和存储敏感支付信息
- PSP 提供接口,负责将信息传输到银行或卡组织
- 公司需要符合 PCI-DSS 等合规标准,风险较高
- 方式二: 托管支付页 (适合不想处理敏感数据的公司)
- 公司不处理敏感数据,由 PSP 提供一个托管支付页面 (Hosted Payment Page )
- 客户输入卡信息后直接提交到 PSP
- 信息安全地存储在 PSP 侧
这是大多数公司采用的方式,因其合规压力小、集成简单。
为了简化展示,图 4 中省略了 Payment Executor (支付执行器 )、Ledger (账本 ) 和 Wallet (钱包 ) 的细节。整个支付流程由 支付服务 (Payment Service )统一协调和编排。
- 步骤 1.用户点击结账
- 用户在浏览器中点击“结账”按钮;
- 客户端将支付订单信息发送给支付服务 (Payment Service )。
- 步骤 2.注册支付请求
- 支付服务收到支付订单信息后,向 PSP 发送 支付注册请求 (Payment Registration Request );
- 请求中包含支付信息,如金额、币种、请求过期时间、支付完成后的跳转链接 (redirect URL )等。
- 为了确保每个支付订单只注册一次,该请求中还包含一个 UUID 字段 (也叫 nonce ),通常直接使用 payment_order 的 ID。
- 步骤 3.获取 token
- PSP 返回一个 token (PSP 侧的唯一标识符 ),是一个 UUID;
- 后续我们可以使用这个 token 查询支付注册和支付执行的状态。
- 步骤 4.存储 token
- 支付服务将 PSP 返回的 token 存入数据库;
- 只有在 token 被持久化后,才允许展示托管支付页面,确保流程一致性。
- 步骤 5.展示托管支付页
- 客户端显示 由 PSP 托管的支付页面 (Hosted Payment Page );
- 移动端一般通过 PSP 提供的 SDK 集成;
- Stripe 提供了一个 JavaScript 库,用于显示支付 UI、收集敏感的支付信息,并直接调用 PSP 完成支付。敏感的支付信息由 Stripe 收集,永远不会到达我们的支付系统。托管的支付页面通常需要两条信息:
token
:支付注册时 PSP 返回的唯一标识符。在第 4 步获取的 token 用于让 PSP 的 JS SDK 拉取支付请求的详细信息。包括支付金额等。redirect URL
:支付完成后 PSP 会重定向到这个 URL,通常是电商网站的订单详情页。
- 步骤 6.用户提交支付信息
- 用户在 PSP 托管的支付页面中填写支付信息,包括:信用卡号, 持卡人姓名, 有效期等
- 填写完毕后,用户点击“支付”按钮,PSP 开始处理支付。
- 步骤 7.PSP 返回支付结果 (同步返回 )
- 支付完成后,PSP 会将支付结果 (成功或失败 )同步返回给前端页面,用于下一步跳转。
- 步骤 8.重定向 到 redirect URL
- 浏览器会根据支付请求中预先配置的 redirect_url,跳转到电商平台的前端页面,例如:
https://your-company.com/?tokenID=JIOUIQ123NSF&payResult=X324FSa
- 支付状态 (如是否成功 )通常会被作为 URL 参数附加在跳转地址后面,供前端页面读取并展示给用户。
- 浏览器会根据支付请求中预先配置的 redirect_url,跳转到电商平台的前端页面,例如:
- 步骤 9.Webhook 异步通知
- 同时,PSP 会通过 Webhook (异步回调 )通知支付系统本次交易的结果
- Webhook 是支付系统预先注册在 PSP 那里的一个 URL;
- PSP 在支付完成后,向该 URL 发起 HTTP 请求,将支付状态推送过来;
- 支付服务接收到回调后,解析其中的 token、支付状态等字段;
- 并在数据库中更新对应订单的
payment_order_status
字段,例如设为SUCCESS
或FAILED
. 这样,即便用户浏览器没有跳转回来,系统也能获知最终的支付状态。
So far, we explained the happy path of the hosted payment page. In reality, the network connection could be unreliable and all 9 steps above could fail. Is there any systematic way to handle failure cases? The answer is reconciliation 对账.
3.2 Reconciliation 对账
在支付系统中,服务之间通常通过异步通信 (如 webhook 回调 )协作。但异步通信具有不确定性:消息可能丢失、重复,或延迟处理,尤其在与 PSP 或银行交互时更为常见。
为确保账务一致性,系统需要引入对账机制 Reconciliation 。对账是指定期比对内部账本记录与外部结算数据(如 PSP 提供的交 易报表),以发现并修复差异。
3.2.1 对账流程简述
- 每日结算文件: PSP 或银行每天发送 settlement file,包含账户余额和当日交易记录;
- 解析结算数据: 对账服务读取并提取交易字段;
- 账本比对: 与系统内的账本记录核对金额、交易状态、交易 ID;
- 标记异常并修复: 如 webhook 丢失或状态不一致;
- 记录日志并触发补偿机制: 可自动修复或人工介入。
3.2.2 对账异常分类与处理方式
Reconciliation is also used to verify that the payment system is internally consistent. For example, the states in the ledger and wallet might diverge and we could use the reconciliation system to detect any discrepancy. 对账也用于验证支付系统内部的一致性。例如,账本和钱包的状态可能存在差异,我们可以使用对账系统来检测任何差异。
- 可分类且可自动修复
- 差异原因明确,修复逻辑清晰,自动化成本低,工程团队可以实现自动识别与修复。
- 示例: 某笔支付在 PSP 侧状态为成功,但系统因 webhook 延迟仍标记为
EXECUTING
. 对账系统识别后可 自动更新为SUCCESS
. 并触发后续流程,如调用钱包服务。
- 可分类但需人工修复
- 差异原因明确,修复方式已知,但处理逻辑较复杂或依赖人工判断,自动化成本高,故由财务团队手动处理。
- 示例: 一笔交易金额与 PSP 文件不一致,原因是客服在后台发起了部分退款,但备注缺失。系统可以识别出异常,但是否应当调整金额仍需人工判断,因此该条记录会放入 job queue,供财务处理。
- 不可分类
- 差异原因不明确,系统无法判断如何修复,需要人工进一步调查。此类异常会被放入特殊队列等待人工介入。
- 示例: 对账文件中出现一笔系统中完全不存在的交易,查不到对应的 payment_order_id、webhook 或日志,可能是重复支付、接口异常或外部错账,需要人工排查数据源或联系 PSP 查明原因。
3.3 Handling payment processing delays 处理支付延迟
- 支付请求可能长时间未完成
- 一个支付请求从发起到完成,通常只需几秒钟。但在某些特殊情况下,支付可能“卡住”,长时间保持未完成状态,甚至延迟数小时或数天。这种情况在实际支付业务中并不少见. 示例:
- PSP (支付服务提供商 )检测到该笔支付存在高风险 (如金额异常、设备异常、地理位置可疑等 ),需要人工干预进行审批。
- 使用的信用卡启用了 3D Secure 认证 (如 Visa Secure、Mastercard Identity Check ),用户需跳转到发卡行页面输入短信验证码或其他身份信息,完成后才能继续支付流程。
- 一个支付请求从发起到完成,通常只需几秒钟。但在某些特殊情况下,支付可能“卡住”,长时间保持未完成状态,甚至延迟数小时或数天。这种情况在实际支付业务中并不少见. 示例:
- 支付服务需支持长时延处理流程
- 系统不能假设所有支付请求都是同步完成的,因此必须支持异步支付状态流转,并提供用户可视化的反馈机制. 示例:
- 用户在支付页面点击“支付”后,系统应能够识别出
PENDING
状态,并显示“正在处理中”,而不是误判为失败或异常。 - 系统需记录
PENDING
状态的订单,并允许用户后续重新进入查询状态。
- 用户在支付页面点击“支付”后,系统应能够识别出
- 系统不能假设所有支付请求都是同步完成的,因此必须支持异步支付状态流转,并提供用户可视化的反馈机制. 示例:
- PSP 托管支付页的处理方式
- 当前大多数公司都采用 PSP 提供的 托管支付页 (hosted payment page ) 来处理支付信息,避免接触敏感卡号、简化 PCI 合规流程。
- 当支付较慢时,PSP 会以以下方式支持整个流程:
- PSP 向前端返回
PENDING
状态 (表示支付已发起,但未最终确认 )。 - 前端根据状态提示用户“支付处理中”,并提供一个状态轮询或结果页面。
- PSP 会在后台持续跟踪支付请求,一旦支付完成 (成功或失败 ),会向支付系统注册的 webhook 地址发送支付结果通知。
- 支付系统收到 webhook 后,更新内部
payment_order_status
,并触发后续逻辑,如发货、通知用户等。
- PSP 向前端返回
- 部分 PSP 不支持 webhook,需轮询处理
- 并不是所有 PSP 都支持 webhook 异步通知。有些 PSP 要求商户系统 自行定期轮询,来检查
PENDING
状态的交易是否已更新。 - 示例:
- 支付服务在检测到某笔交易为
PENDING
后,设置一个轮询任务 (例如每 5 分钟一次 ),调 用 PSP 的订单状态查询接口。 - 如果查询结果变为
SUCCESS
或FAILED
,则更新系统订单状态,并终止轮询任务。 - 如果超过一定时间仍未完成,可进入异常处理流程 (例如触发告警、让用户重新支付等 )。
- 支付服务在检测到某笔交易为
- 并不是所有 PSP 都支持 webhook 异步通知。有些 PSP 要求商户系统 自行定期轮询,来检查
3.4 Communication among internal services 内部服务通信
A. 同步通信 Synchronous
- 使用 HTTP 等方式,适合小规模系统。
- 缺点:
- 性能差:任一服务慢都会拖垮整体链路;
- 故障不隔离:外部服务如 PSP 宕机会直接导致请求失败;
- 强耦合:调用方需知道接收方接口;
- 难扩展:没有队列缓冲时,流量突增难处理。
B. 异步通信 Asynchronous
异步通信可提高系统弹性,常用两种模式:
- 单接收者 (Single Receiver )
- 通过消息队列实现,每条消息仅由一个服务消费;
- 例如服务 A 和 B 订阅同一队列,消息 m1 给 A、m2 给 B,消费后即删除。
- 多接收者 (Multiple Receivers )
- 使用 Kafka 等系统,消息不会被删除,可被多个服务消费;
- 适合支付场景:一次支付事件可同时触发推送、报表更新、风控审计等。
3.5 Handling failed payments 处理支付失败
Every payment system has to handle failed transactions. Reliability and fault tolerance are key requirements. 在支付系统中,失败是常态而非例外,系统的可靠性与容错机制至关重要。本节介绍几种常见的失败处理策略。
3.5.1 支付状态追踪 (Tracking Payment State )
- 任何时刻都应能明确知道支付处于哪个阶段;
- 每次状态变更都记录到 追加写表 append-only table
- 一旦出错,系统可基于状态判断是否应重试、退款或中止处理。
追加写表指的是只插入 (INSERT )新记录,不修改 (UPDATE )或删除 (DELETE )已有数据
- 每条记录表示支付状态的一次变更,形成完整的历史轨迹.
- 该设计 有助于调试、审计及失败场景的溯源.
- 示例:一笔交易可能经历 INITIATED → EXECUTING → FAILED,多条记录保留各阶段信息.
- 一旦出错,系统可基于状态判断是否应重试、退款或中止处理.
假设我们要记录一笔支付交易的状态变化过程:
payment_id | status | updated_at |
---|---|---|
pay_001 | INITIATED | 2025-07-05 10:00:00 |
pay_001 | EXECUTING | 2025-07-05 10:01:12 |
pay_001 | FAILED | 2025-07-05 10:01:45 |
3.5.2 重试队列与死信队列 (Retry Queue & Dead Letter Queue )
- Retry Queue
- 处理临时性错误 (如网络波动、服务短暂不可用 );
- 消息会被自动重试,直到成功或超过最大次数。
- Dead Letter Queue (DLQ )
- 消息在重试多次仍失败后进入 DLQ;
- 用于问题定位和排查,比如格式不合法、业务数据缺失等;
- DLQ 是系统稳定性的重要保障,可以避免异常消息阻塞主流程。
3.5.3 Failed Payment Retry Flow 支付失败重试流程
支付失败后,系统根据错误类型和重试次数采取不同策略:
- 1.判断是否可重试
- 可重试错误 (如网络异常 ) → 路由到 Retry Queue
- 不可重试错误 (如参数非法 ) → 错误信息写入数据库,供后续分析
- 2.消费 Retry Queue 并执行重试逻辑
- 系统异步消费重试队列,对失败的支付事务进行重试
- 3.重试后仍失败的处理分支
- 未超过最大重试次数 → 继续路由回 Retry Queue,等待下一轮重试
- 超过最大重试次数 → 路由到 Dead Letter Queue,供人工调查或后续自动处理
实际案例:Uber 的支付系统使用 Kafka 实现了上述机制,满足高可靠与高容错要求。
3.6 Retry 重试
由于网络异常或超时,支付请求可能失败,系统需要通过重试机制实现 至少一次 at-least-once 的保障。
示例: 用户发起 10 美元支付请求,由于网络不稳定,前三次失败,第四次成功。这种情况在弱网环境中很常见。
常见重试策略如下:
- 立即重试 Immediate Retry:失败后立即重试;
- 固定间隔 Fixed Intervals:每次重试之间间隔固定时间 (例如 5 秒)
- 增量间隔 Incremental Intervals:初次重试等待时间较短,之后逐渐增加;
- 指数退避 Exponential Backoff:每次失败后等待时间翻倍 (例如 1 秒 → 2 秒 → 4 秒);
- 取消 Cancel:当错误是永久性的或进一步重试没有意义时,终止请求。
选择何种策略应根据场景而定。若网络问题短时间内无法解决,建议使用指数退避。过于频繁的重试会浪费资源并可能压垮系统。
最佳实践是返回带有 Retry-After
响应头的错误码,引导客户端合理重试。
支付重试可能导致重复扣款,常见场景包括:
- 用户在 PSP 托管支付页面中连续点击支付按钮;
- PSP 侧已成功处理支付,但由于网络问题客户端未收到响应,用户或客户端再次发起支付请求。
为避免重复支付,系统需实现 至多一次 at-most-once 的保障机制,即 幂等性 Idempotency.
3.7 Idempotency 幂等性
幂等性是实现 至多一次 at-most-once 保证的关键。根据维基百科定义,幂等性是指某些操作可以重复执行多次,其结果与第一次执行时相同,不会引起额外变化。 在 API 语义上,幂等性意味着客户端可以多次发出相同请求,而系统只处理一次,最终效果保持一致。
在客户端(如网页或移动端)与服务器通信时,通常使用一个由客户端生成的幂等性 Key,具有唯一性并设置一定的过期时间。UUID 是常见选择,许多公司(如 Stripe 和 PayPal)推荐这 样做。 在发起支付请求时,将幂等性 Key 添加到 HTTP 请求头中,例如:
POST /v1/payments HTTP/1.1
Host: payments.example.com
Content-Type: application/json
idempotency-key: 9f5c2b10-a7d8-4b91-a303-78e6c5fe01ff
{
// request payload
}
3.7.1 幂等性如何避免重复支付?
场景 1.用户快速点击两次“支付”按钮
- 当用户第一次点击“支付”时,客户端向支付系统发送一个包含幂等性 key(如 UUID)的 POST 请求。支付系统处理该请求并返回支付成功信息。
- 如果用户在短时间内再次点击“支付”,客户端会再次发送相同幂等性 key 的请求。支付系统检测到该 key 已处理过,于是不会重复处理,而是直接返回第一次的处理结果。
- 如果同时有多个请求使用同一个幂等性 key,系统只处理第一个,其他返回
429 Too Many Requests
错误码。 - 可使用数据库主键或唯一约束来支持幂等性。具体逻辑:
- 接收到支付请求时尝试插入一条记录
- 如果插入成功,说明是首次处理
- 如果插入失败(主键冲突),说明请求已处理过,可直接返回之前的响应结果.
场景 2.支付成功但响应未送达
- 若支付请求成功到达 PSP 并完成扣款,但由于网络问题,PSP 的响应未能返回给支付 系统,用户再次点击“支付”。
- 这时,支付系统向 PSP 发送相同的 nonce,PSP 返回的 token 也与第一次相同(token 与 nonce 一一对应)。因此,PSP 能识别为重复支付请求,并返回第一次的处理状态,避免二次扣款。
{
"amount": "19.99",
"currency": "USD",
"nonce": "checkout_abc_20250705"
}
3.7.2 nonce 的作用
nonce 是 “number used once” 的缩写,意思是 “只使用一次的数字”。
- 防重放攻击(replay attack) 每次发起请求都附带一个唯一的 nonce,如果攻击者拦截并重复发送旧请求,系统会识别出 nonce 已使用过,从而拒绝执行。
- 作为幂等性标识符(Idempotency Identifier) 在支付中,nonce 通常是客户端生成的唯一 ID,用来标识一笔支付请求。即使用户多次点击“支付”,只要 nonce 相同,系统就知道这些是同一笔交易,避免重复扣款。
- 与 PSP 的 token 建立映射
- nonce → 客户端和支付服务生成的唯一请求 ID
- token → PSP 接收到 nonce 后返回的唯一标识(供后续调用使用)
- 二者绑定,确保支付请求的唯一性和安全性
对象 | 概念 | 使用位置 | 谁生成 | 幂等作用在哪 |
---|---|---|---|---|
idempotency-key | 客户端幂等键 | 客户端 ➝ 我们系统 | 客户端 | 我们系统识别重试 |
nonce | PSP 幂等键 | 我们系统 ➝ PSP | 我们服务端 | PSP 识别重试 |
3.8 Payment security 支付安全
问题 | 中文解释 | 解决方案 |
---|---|---|
Request/response eavesdropping | 请求/响应被窃听 | 使用 HTTPS 加密通信 |
Data tampering | 数据篡改 | 强制使用加密和完整性监控 |
Man-in-the-middle attack | 中间人攻击 | 使用 SSL 并配合证书固定(certificate pinning) |
Data loss | 数据丢失 | 在多个区域进行数据库复制,并定期快照 |
DDoS attack | 分布式拒绝服务攻击 | 设置限流与防火墙策略 |
Card theft | 信用卡信息被盗 | 使用 Tokenization (令牌化),即用虚拟 token 替代真实卡号进行支付 |
PCI compliance | PCI 合规性要求 | 遵循 PCI DSS 标准,这是处理信用卡的组织需遵守的信息安全规范 |
Fraud | 欺诈行为 | 使用地址验证(AVS)、CVV 校验、用户行为分析等反欺诈手段 |
Step 4 - Wrap Up
A payment system is extremely complex. Even though we have covered many topics, there are still more worth mentioning. 支付系统极其复杂。尽管我们已经讨论了很多主题,但仍然有更多值得一提的。
- Monitoring 监控
- 监控关键指标是任何现代应用程序的关键部分。通过广泛的监控,我们可以回答诸如“特定支付方式的平均接受率是多少?”“我们服务器的 CPU 使用率是多少?”等问题。我们可以在仪表板上创建并显示这些指标。
- Golden Metrics (Latency, Traffic, Errors, Saturation 系统资源的使用程度) 是监控的核心指标。
- Grafana, Prometheus, AlertManager...
- Alerting 警报
- 当发生异常情况时,必须向值班开发人员发出警报,以便他们及时做出响应。
- Debugging tools 调试工具
- "为什么付款会失败?"是一个常见问题。
- 为了方便工程师和客服人员进行调试,开发一些工具,方便工作人员查看付款交易的交易状态、处理服务器历史记录、PSP 记录等信息至关重要。
- Currency exchange 货币兑换
- 在为国际用户群设计支付系统时,货币兑换是一个重要的考虑因素。
- Geography 地理位置
- 不同地区可能有完全不同的支付方式。
- 中国: 微信、支付宝
- Cash payment 现金支付
- 现金支付在印度、巴西和其他一些国家非常普遍。Uber[28]和 Airbnb[29]撰写了详细的工程博客,介绍了他们如何处理现金支付。
- 集成 Google/Apple Pay