首页 › AI System Design › 第26章:设计虚拟支付系统

第26章:设计虚拟支付系统

简介​

在本章中,我们将设计一个支付系统,它支撑着当今所有的电子商务.​

A支付系统用于结算金融交易,实现资金的转移。​

​

​

第一步:理解问题并明确设计范围​

•

C:我们正在构建什么样的支付系统?​

•

我:一个面向电子商务系统的支付后端,类似于Amazon.com。它负责处理所有与资金流动相关的事务。​

•

C:支持哪些支付方式——信用卡、PayPal、银行卡等吗?​

•

我:系统在实际应用中应支持所有这些支付方式。出于面试的考虑,我们可以采用信用卡支付。​

•

答:我们自己处理信用卡支付吗?​

•

问:不,我们使用像Stripe、Braintree、Square等这样的第三方支付服务商。​

•

C:我们在系统中存储信用卡数据吗?​

•

I:出于合规性考虑,我们不会在系统中直接存储信用卡数据,而是依赖第三方支付处理机构。​

•

C:该应用是全球性的吗?我们需要支持多种货币和国际支付吗?​

•

我:该应用是全球性的,但为面试目的,我们假设仅使用一种货币。​

•

C:我们每天支持多少笔支付交易?​

•

我:每天100万笔交易。​

•

C:我们是否需要支持每月向付款方进行资金支付的流程?​

•

我:是的,我们需要支持这一点​

•

C:还有其他需要注意的事项吗?​

•

I:我们需要支持对账功能,以修复与内部及外部系统交互时可能出现的不一致问题。​

Functional requirements​

•

收款流程——支付系统代表商家接收客户付款​

•

付款流程——支付系统向全球各地的卖家汇款​

Non-functional requirements​

•

可靠性与容错性。失败的支付需要得到妥善处理​

•

需要建立内部系统与外部系统之间的对账机制。​

Back-of-the-envelope estimation​

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

这对于任何数据库系统而言都不是高吞吐量,因此并非本次面试的重点。​

​

​

步骤2:提出高层次设计方案并获得认可​

从宏观层面来看,参与资金流动的主体共有三个:​

​

飞书文档 - 图片

​

Pay-in flow​

以下是入账流程的高层次概览:​

​

飞书文档 - 图片

​

•

支付服务——接收支付事件并协调整个支付流程。通常还会通过第三方服务商对反洗钱违规或犯罪活动进行风险审核。​

•

支付执行方——通过支付服务提供商(PSP)执行单笔支付指令。支付事件可能包含多笔支付指令。​

•

支付服务提供商(PSP)——负责将资金从一个账户转移到另一个账户,例如从买家的信用卡账户转入电商平台的银行账户。​

•

卡组织——负责处理信用卡业务的机构,例如Visa、MasterCard等。​

•

账本——记录所有支付交易的财务信息。​

•

钱包——维护所有商户的账户余额。​

以下是入账流程示例:​

•

用户点击“下单”,一个支付事件会被发送到支付服务​

•

支付服务将该事件存储在其数据库中​

•

支付服务会为所有支付订单调用支付执行器,这是支付事件的一部分​

•

支付执行器将其支付订单存储在其数据库中​

•

支付执行器调用外部支付服务提供商(PSP)以处理信用卡支付​

•

在支付执行器处理完付款后,支付服务会更新钱包,记录卖家的余额​

•

钱包服务将其更新后的余额信息存储在其数据库中​

•

支付服务调用账本以记录所有资金流动​

APIs for payment service​

​

​

示例payment_order:​

​

​

注意事项:​

•

这是一个名为“system-design-notes/01. Scaling at main · liquidslr/system-design-notes”的GitHub仓库页面。上下文。​

•

这是一个 GitHub 仓库页面,标题为“system-design-notes/01. Scaling at main · liquidslr/system-design-notes”。上下文。​

​

代码块​

Plain Text

GET /v1/payments/{:id}​

​

该端点根据以下内容返回单笔支付的执行状态payment_order_id.​

Payment service data model​

这是一个 GitHub 仓库页面,标题为“system-design-notes/01. Scaling at main · liquidslr/system-design-notes”。上下文。​

对于支付而言,性能通常并不是一个重要因素。然而,强一致性却至关重要。​

选择数据库时的其他考量:​

•

拥有大量可聘用的数据库管理员来管理数据库​

•

经验证的业绩记录,表明该数据库已被其他大型金融机构使用​

•

配套工具的丰富程度​

•

为确保ACID特性而采用传统SQL而非NoSQL/NewSQL​

以下是payment_events表格包含的内容:​

•

checkout_id- 字符串,主键​

•

buyer_info- 字符串(个人备注——可能使用指向另一张表的外键更为合适)​

•

seller_info- 字符串(个人笔记 - 同上)​

•

credit_card_info- 取决于卡片提供商​

•

is_payment_done- 布尔值​

以下是payment_orders表格包含的内容:​

•

payment_order_id- 字符串,主键​

•

buyer_account - string​

•

amount - string​

•

currency- 字符串​

•

checkout_id- 字符串,外键​

•

payment_order_status - enum (NOT_STARTED, EXECUTING, SUCCESS, FAILED)​

•

ledger_updated- 布尔值​

•

wallet_updated- 布尔值​

注意事项:​

•

有许多付款订单,与某个特定的支付事件相关联​

•

我们不需要seller_info用于入账流程。那仅在出账时才需要​

•

这是一个 GitHub 仓库页面,标题为“system-design-notes/01. Scaling at main · liquidslr/system-design-notes”。上下文。​

•

支付状态的转换由后台任务负责管理,该任务会定期检查进行中的支付状态,并在某笔支付在合理时间内仍未处理时触发告警​

Double-entry ledger system​

复式记账机制是任何支付系统的核心。这是一种通过始终对两个账户同时执行资金操作来追踪资金流动的机制,其中一个账户余额增加(贷方),另一个账户余额减少(借方):​

​

​

所有交易记录的总和始终为零。这一机制确保了系统内所有资金流动的端到端可追溯性。​

Hosted payment page​

为避免存储信用卡信息并应对各项繁重的监管要求,大多数公司更倾向于使用由支付服务提供商(PSP)提供的支付小部件,由其代为存储并处理信用卡支付:​

​

​

Pay-out flow​

支付流程的各个组成部分与收款流程非常相似。​

主要区别:​

•

资金从电商平台的银行账户划转至商家的银行账户​

•

我们可以借助第三方应付账款服务商,例如Tipalti​

•

在资金支付方面,还需要处理大量的账务记录和监管要求。​

​

​

步骤3:设计深度剖析​

本节重点在于提升系统的性能、可靠性和安全性。​

PSP Integration​

如果我们的系统能够直接与银行或卡组织对接,则无需通过支付服务提供商即可完成支付。这类直连极为罕见且不常见,通常只在那些能够证明投资合理性的大型企业中才会实现。​

如果走传统路线,PSP 可以通过以下两种方式之一进行集成:​

•

通过 API,前提是我们的支付系统能够收集支付信息​

•

通过托管支付页面,以避免处理支付信息相关法规​

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

​

​

•

用户在浏览器中点击“去结算”按钮​

•

客户端将支付订单信息发送至支付服务​

•

收到付款指令信息后,支付服务会向PSP发送付款登记请求。​

•

PSP 会接收付款信息,如币种、金额、有效期等,以及用于幂等性的 UUID。通常为该笔支付订单的 UUID。​

•

PSP 返回一个用于唯一标识支付注册的令牌,该令牌将存储在支付服务的数据库中。​

•

令牌存储完成后,系统会为用户呈现由 PSP 托管的支付页面。该页面将使用令牌以及用于成功或失败后的重定向 URL 进行初始化。​

•

用户在PSP页面填写支付信息,PSP处理支付并返回支付状态​

•

用户现已重定向回 redirectURL。示例重定向 URL -https://your-company.com/?tokenID=JIOUIQ123NSF&payResult=X324FSa​

•

异步地,PSP通过Webhook调用我们的支付服务,将支付结果通知至我们的后端​

•

支付服务根据接收到的Webhook记录支付结果​

Reconciliation​

上一节介绍了支付的正常流程。对于异常情况,将通过后台对账机制进行检测与处理。​

每晚,PSP都会发送一份结算文件,我们的系统会利用该文件将外部系统的状态与内部系统的状态进行比对。​

​

​

这一流程也可用于检测账本与钱包服务之间存在的内部不一致问题。​

不匹配由财务团队手动处理。不匹配的处理方式如下:​

•

可归类,因此属于已知的不匹配,可通过标准流程进行调整​

•

可分类,但无法实现自动化。需由财务团队手动调整​

•

无法归类。已由财务团队手动调查并调整​

Handling payment processing delays​

有些情况下,一笔付款可能需要数小时才能完成,尽管通常只需几秒钟。​

这可能由以下原因导致:​

•

一笔付款被标记为高风险,需要人工进行审核​

•

信用卡需要额外的安全保护,例如3D安全认证,这需要持卡人提供额外的信息才能完成​

这些情况由以下方式处理:​

•

等待支付服务提供商在付款完成后向我们发送 Webhook,或在其不提供 Webhook 时轮询其 API​

•

向用户显示“待处理”状态,并为其提供一个页面,以便其查看付款更新。我们还可以在用户付款完成后向其发送一封电子邮件​

Communication among internal services​

服务之间进行通信时,通常有两种模式——同步与异步。​

同步通信(如 HTTP)在小规模系统中表现良好,但随着规模扩大便会面临诸多问题:​

•

性能低下——随着调用链中涉及的服务增多,请求-响应周期变得漫长​

•

故障隔离性差——如果PSP或其他任何服务发生故障,用户将无法收到响应​

•

紧耦合——发送方需要了解接收方​

•

难以扩展——由于缺乏缓冲,难以应对流量的突然增长​

异步通信可分为两类。​

单个接收者——多个接收者订阅同一主题,消息仅被处理一次:​

​

​

多个接收者——多个接收者订阅同一个主题,但消息会被转发给所有接收者:​

​

​

后一种模型非常适用于我们的支付系统,因为一笔支付可能会触发多种副作用,由不同的服务分别处理。​

简而言之,同步通信更为简单,但无法实现服务的自治。异步通信则以牺牲简单性和一致性为代价,换取更高的可扩展性和系统韧性。​

Handling failed payments​

每个支付系统都必须处理支付失败的情况。以下是我们将采用的一些机制:​

•

跟踪支付状态——每当支付失败时,我们都可以根据支付状态决定是否重试或退款。​

•

重试队列——我们将重试的支付记录发布到重试队列​

•

死信队列——已发生永久性失败的支付会被推送到死信队列,在那里可以对失败的支付进行调试和检查。​

​

​

Exactly-once delivery​

我们需要确保一笔支付仅被处理一次,以避免对客户重复扣款。​

如果一个操作在同时满足至少执行一次和至多执行一次的条件下被执行,则该操作即为恰好一次执行。​

为了实现至少一次的保证,我们将采用重试机制:​

​

​

以下是确定重试间隔的一些常见策略:​

•

立即重试——客户端在失败后立即发送另一条请求​

•

固定间隔——在重试付款前等待一段固定的时间​

•

递增间隔——在每次重试之间逐步增大重试间隔​

•

指数退避——每次重试之间的间隔时间翻倍​

•

取消 - 客户端取消了请求。这种情况通常发生在错误为终端错误或已达到重试阈值时​

这是一个名为“system-design-notes/01. Scaling at main · liquidslr/system-design-notes”的GitHub仓库页面。上下文。 Retry-After​

重试的一个问题是,服务器可能会重复处理同一笔付款:​

•

客户端两次点击了“支付按钮”,因此被扣款两次​

•

支付已由支付服务提供商(PSP)成功处理,但下游服务(账本、钱包)未处理。重试会导致支付再次被PSP处理​

为了解决重复支付问题,我们需要采用幂等性机制——即多次执行某项操作时,仅会被处理一次的特性。​

这是一个名为“system-design-notes/01. Scaling at main · liquidslr/system-design-notes”的GitHub仓库页面。上下文。​

​

​

可以通过数据库的唯一键约束机制实现幂等性:​

•

服务器尝试向数据库插入一行新数据​

•

由于违反了唯一键约束,插入操作失败​

•

服务器检测到该错误后,改为将现有对象返回给客户端​

幂等性同样在支付服务提供商(PSP)端得到应用,借助此前已讨论过的随机数(nonce)。PSP会确保不会对具有相同随机数的支付请求进行重复处理。​

Consistency​

在支付的整个生命周期中,会调用多个有状态的服务——支付服务提供商(PSP)、账本、钱包以及支付服务。​

任意两个服务之间的通信都可能失败。我们可以通过实现“恰好一次”处理与数据一致性校验,确保所有服务之间最终的数据一致性。​

如果我们采用数据复制,就必须应对复制延迟问题,这可能导致用户在主库与副本库之间看到不一致的数据。​

为缓解这一问题,我们可以让所有读写操作都由主数据库处理,仅在需要冗余和故障转移时才使用副本。此外,我们也可以通过采用 Paxos 或 Raft 等一致性算法,确保副本始终与主节点保持同步。我们也可以使用基于共识的分布式数据库,例如 YugabyteDB 或 CockroachDB。​

Payment security​

以下是一些可用于确保支付安全的机制:​

•

请求/响应窃听——我们可以使用HTTPS来保障所有通信的安全​

•

数据篡改——强制实施加密并进行完整性监控​

•

中间人攻击——使用SSL证书固定​

•

数据丢失——在多个区域之间复制数据并定期创建数据快照​

•

DDoS攻击——实施速率限制与防火墙​

•

卡片盗刷——在系统中使用令牌而非存储真实的卡片信息​

•

PCI合规性——一项针对处理品牌信用卡的组织的安全标准​

•

欺诈——地址验证、卡验证码(CVV)、用户行为分析等​

​

​

步骤4:总结​

其他讨论要点:​

•

监控与告警​

•

调试工具——我们需要能够帮助我们轻松理解支付失败原因的工具​

•

货币兑换——在设计面向国际使用的支付系统时至关重要​

•

地域差异——不同地区可能采用不同的支付方式​

•

现金支付——在印度、巴西等地非常普遍​

•

Google/Apple Pay 集成​