当前位置: 首页 > article >正文

面试场景题系列:设计支付系统

从网约车、旅游、外卖到电商或者医疗保健,幕后运行的支付系统让所有的经济活动变成可能。近年来,设计一个可靠、可扩展、灵活的支付系统成为流行的系统设计面试题。

什么是支付系统?维基百科上是这么说的,“支付系统是通过转移货币价值来完成金融交易的系统,包括促成交易的机构、工具、人员、规则、程序、标准和技术。”支付系统被广泛使用。从表面上来看,支付系统很容易理解,但是对大部分开发人员来说它很可怕,因为一个很小的问题就可能导致重大的收入损失及用户流失。在本文中,我们会解密支付系统。

1.场景界定

第一步 理解问题并确定设计的边界对不同的人来说,支付系统可能有非常不同的含义。有些人可能会认为它是像Apple Pay或者Google Pay之类的电子钱包;有些人可能认为它是一个处理支付的后端系统;还有一些人认为它是支持PayPal或者信用卡支付的系统。从通用性考虑,本文我们聚焦于设计一个能支持如下使用场景的支付系统。

•收款流程:支付系统代表卖家向顾客收款。

•付款流程:支付系统每个月给全世界的卖家付款。

•支持提到的所有主流支付方式

•系统每天交易笔数:100万笔

•实时卖家仪表板:显示卖家将要收到的款项。仅了解顾客的使用场景并不足以提出可靠的设计。

为了设计出可以处理数百万笔交易的支付系统,理解电商商家可能面临的挑战很重要。

2018年MRC Global Payments Survey做了一次调查,将商家管理电商支付时遇到的挑战做成了一个排行榜(如图-1所示)。在我们的设计中,我们会把一些排名靠前的挑战考虑进来。

图-1

2 顶层设计

在高层级上,这个系统可以分成3个部分:

•收款流程。

•付款流程。

•实时卖家仪表板。

2.1 收款流程

我们先看一下当顾客在亚马逊下单之后会发生什么,如图-2所示。

图-2

•顾客:将产品添加到购物车中。

•订单系统:创建订单。

•支付系统:记录资金的流动,但是并没有真正地转移资金。

•支付服务处理器(Payment Service Processor,PSP):PSP把资金从账户A转移到账户B。“PSP”这个词在本文中也代表银行。请注意,真实的资金流动过程很复杂,图-2是对现实的简化和抽象。

流程的抽象

亚马逊不仅仅有购物网站,它还有超过40家子公司,包括Audible、IMDB、Zappos、AWS等[插图]。其中的一些子公司可能共享同一个支付系统。此外,亚马逊还有不同的业务线,比如购物、亚马逊Prime视频、亚马逊音乐等。为了支持所有这些业务,我们对支付流程相关的组件进行抽象,并提供一个如图-3所示的简单模型。

图-3

•业务事件:业务事件触发资金的流动。这些事件包括顾客下单、订阅亚马逊Prime视频等。

•支付系统:之前已经解释过了。

•PSP:之前已经解释过了。

这里会有多种类型的业务事件和很多不同的PSP,如图-4所示。

图-4

当我们设计支付系统时,很重要的一点是把不断变化的组件与稳定的组件分开。举个例子,亚马逊不断拓展新的市场并增加新业务线。新的业务线通常意味着新的支付业务事件。现在没有单一的PSP可以接受所有的支付方式。为了支持国际支付,你需要添加更多的支付服务提供商。

我们看看收款流程的哪些部分是稳定的,哪些部分是不断变化的。收款流程有3个组件:支付API、支付核心和支付传输(见图-5)。

图-5

支付API:隐藏了系统其他组件的复杂性,并为业务事件提供了高层级接口。这个组件应该具有可扩展性以容纳新的业务事件。

支付核心:代表系统的核心。这个组件通常是稳定的。

支付传输:提供了支付路由以及与不同PSP的集成。这个组件应该具有可扩展性以接纳新的PSP。

2.2 复式记账系统(Double-Entry System)

在继续详解高层级设计之前,让我们看一个重要概念:复式记账系统(也叫作复式记账/簿记)[插图]。复式记账系统是支付系统的基础,并且对记录资金的流动很关键。它把每笔支付交易记录在两个相互独立的账户中,金额相同,一个账户借记,另一个账户就会贷记相同金额(见图-6)。

复式记账系统声明所有交易分录之和必须等于0。每一个账户中损失的金额(比如,失去1分钱),必然对应在另一个账户中获得的相同金额(比如,得到1分钱)。它提供了端到端的可追踪性并确保了整个支付周期的一致性。

图-6

2.3 托管支付页面

大部分中小型公司倾向于不存储顾客的信用卡账户信息,因为如果要这么做,就必须遵守支付卡行业数据安全标准(Payment Card Industry Data Security Standard,PCI DSS)中的所有复杂规定。为了避免处理信用卡信息,这些公司会使用由PSP提供的托管信用卡页面。在网站上,它可能是一个窗口小组件或者iframe;在移动应用中,它可能是一个支付SDK预先构建的页面。图-7是一个集成了PayPal的结账界面。这里的关键点是PSP提供了一个托管支付页面,可以直接获取顾客的信用卡信息而不是由支付系统来获取。

图-7

高层级架构

收款流程的高层级架构如图-8所示。

图-8

我们先仔细看每个组件,然后理解它们是如何一起工作的。圆圈里的数字代表处理支付时的流程顺序。

业务事件

如之前解释的那样,资金的流动是通过业务事件来触发的。

支付API

我们的支付API支持幂等性,所以顾客可以安全地重试支付请求而不用担心会重复付款。有关幂等性的详细内容会在3.1节介绍。最常用的支付API有如下4个。

(1)创建支付:请求付款。

POST /v1/payments

请求头(Header)和路径参数:amount(金额)、currency(币种)

(2)重试支付:因为网络故障或者超时而重试支付操作。

POST /v1/payments

添加额外的幂等性键Idempotency-Key:<key>header,避免做重试操作的时候向顾客收两次钱。

(3)获取支付细节:获取特定支付的相关信息。

GET /v1/payments/{id}

(4)退款:把钱款退回顾客。

POST /v1/payments/{id}/rufunds

嵌入的{id}表示某支付交易的ID。

上面提到的支付API和PSP API类似。如果你想更全面地了解支付API,请参阅Stripe官网上的API文档。

支付处理器

支付处理器协调和管理不同的支付服务。它把支付系统其他服务的复杂性都隐藏起来。它是一个提供如下功能的编排层。

•调用反欺诈和风险服务,检查支付中是否存在欺诈行为。

•调用路由服务,决定哪个PSP对于支付交易是最好的选择。

•调用支付集成服务,处理支付交易。反欺诈和风险服务反欺诈和风险服务接受正常的交易并拒绝欺诈性交易。支付生命周期的任何阶段都可能存在风险,所以这个服务在一个支付交易中可能会被请求很多次。

反欺诈和风险服务

反欺诈和风险服务接受正常的交易并拒绝欺诈性交易。支付生命周期的任何阶段都可能存在风险,所以这个服务在一个支付交易中可能会被请求很多次。

路由服务

路由服务定义了支付路由规则,并动态地将支付交易路由到最合适的PSP。

•降低支付处理的成本。不同的PSP和银行收取的支付处理费用不同,路由服务会尽量最小化这个费用。

•最大化成功率。如果一个支付交易失败,路由服务会选择另一个PSP来处理这个失败的支付交易。

•减小故障的影响。路由服务会自动检测故障,并把交易从发生故障的PSP切换到备用的PSP。

•降低支付处理的延时。路由服务紧密地监测交易延时,并选择响应快的PSP。

支付集成服务

没有一个全球性的PSP的服务可以覆盖所有国家和支付方式。因此,跨国运营的公司必须与多个PSP集成。支付集成服务提供统一的API来与第三方PSP和银行进行通信。一般而言,支付集成服务通过PSP与银行进行通信,但是有些银行自己就是PSP。如图-9所示,两种类型的通信协议被广泛使用。

图-9

•实时API集成(采用HTTPS协议)。大部分PSP(如PayPal、Stripe等)为支付集成服务提供API,用于实现实时支付。

•批量文件集成(采用SFTP协议)。这种方式不是实时的,支付请求有可能需要几天时间来处理。很多银行只提供使用安全文件传输协议(Secure File Transfer Protocol,SFTP)来集成的方式。SFTP有它的优势,可以异步处理大量的数据。我们的支付集成服务将每天的交易(或按照其他预设的频率)汇总到文件里,并将这些文件批量发送给银行。这里的一个重要考量是确定性批处理(Deterministic Batching),意思是即使多次运行同样的文件生成任务,结果也是一样的。如果服务器崩溃,只需重新运行这个任务。

PSP和银行

根据维基百科上的定义,“支付服务提供商(Payment Service Provider,PSP)为在线购物提供服务来接受电子支付,有多种支付方式,包括信用卡、基于银行的支付(比如直接借记、银行转账)和基于网上银行的实时银行转账”。简而言之,PSP和银行负责实际的资金收付。

数据层

我们选择关系型数据库来存储支付数据,因为它提供了ACID特性。一些最重要的数据库列举如下:交易日志(Transaction Log)数据库、令牌保险库(Token Vault)、支付档案(Payment Profile)和用户档案(User Profile)。

交易日志数据库

所有支付交易都存储在交易日志数据库中。它是一个关系型数据库,因为ACID特性对于支付数据很重要。维护数据完整性是很有挑战性的,但极度重要。第3节讲述了更多细节。

令牌保险库

令牌保险库是一个安全地集中存储支付令牌的地方。它也是一个关系型数据库。支付令牌化是用唯一的支付令牌来替换敏感的信用卡或者银行账户细节信息的过程(如图-10所示)。因为不用暴露消费者的信用卡信息,令牌化让无卡支付变为可能。安全是最重要的。令牌保险库必须符合PCI规程。

图-10

支付档案

支付档案存储如下信息。

•支付方式:信用卡、借记卡、银行账户、PayPal等。

•订阅和定期支付(Recurring Payment)数据。

•支付方式对应的账户持有人的名字和账单地址。

图-11展示了创建支付档案的例子。尽管这里输入了卡号,但是我们的支付系统并不存储完整的卡号,而通常只存储最后4位和信用卡的到期日期。只有在传输或者完整保存时,主账号(Primary Account Number,PAN)才会被视为敏感数据。

图-11

用户档案

用户档案存储了用户数据,包括姓名、密码、地址等。

缓存

在支付系统中,读请求(例如检查支付状态)通常比实际的交易支付请求多。使用缓存不仅可以减少数据库和核心支付服务的负载,还可以显著提升读速度

2.4 付款流程

付款流程指的是亚马逊按照预先设定的频率(比如每个月一次)给卖家付款的流程。付款流程如图-12所示。

图-12

1.支付金额汇总在支出表里。

2.调度器从汇总的支出表中获取支出金额。

3.调用支付API请求付款给卖家。

4.把钱付给卖家。这个流程和图16-9中的收款流程一样:支付API请求支付处理器,并最终请求PSP转移资金。

2.5 实时卖家仪表板

除了收款和付款流程,卖家仪表板是我们想要支持的第三个功能。卖家仪表板展示卖家在下一个付款周期将要收到的总金额的实时数据。图-13展示了利用Apache Kafka支持实时卖家仪表板的设计。Apache Kafka是一个发布—订阅消息系统。它很快,具有高可扩展性、可用性以及对节点故障的容错性。Kafka类似于消息队列,但有几个关键区别。

•对于消息队列,消息/事件一旦被处理,就将从队列中被移除。一个消息/事件是由一个消费者处理的。

•对于Kafka,消息/事件被处理后并没有从Kafka中被移除,而是停留在队列中,直到队列大小超过了限制。这使我们能重新处理消息/事件。

图-13

因为同一个支付事件通常由多个下游服务来处理,比如付款流程、报告流水线、数据分析服务、会计等,所以很合适用Kafka。在创建实时仪表板时,经常要用到Vertica、Druid、Amazon Redshift、Google BigQuery等分析型数据库。分析型数据库是为数据分析优化过的专业数据库。它们针对查询性能和可扩展性做了优化。

3. 深入设计

在本节中,我们会重点讨论如何使系统更加健壮和安全。在分布式系统中,错误不仅无法避免,还很常见。举个例子,如果顾客多次点击付款按钮会发生什么?他会被多次收费吗?如何处理因为网络连接差而造成的支付失败?在本节中,我们会深入探讨如下关键话题。

•重试:至少一次交付。

•幂等:至多一次交付。

•维持一致性。

•修复不一致的数据。

•处理支付失败。

3.1 重试和幂等

为了确保顾客只被收费一次,我们需要确保支付交易至少发生一次,至多也只发生一次。你可能会疑惑为什么我们不直接说就发生一次。这是因为“至少一次”和“至多一次”解决的是两个不同的技术问题。

重试

因为网络故障或者超时,我们偶尔需要重试支付交易。重试可以保证支付交易至少发生一次。例如,如图-15所示,客户端(顾客)尝试请求支付10美元,但是因为网络连接不佳,支付总是失败。考虑到网络状况过一段时间可能会变好,客户端重试这个请求,并且在第4次的时候终于成功。

确定合适的重试时间间隔很重要。这里有一些常见的重试策略。

•立即重试:客户端立即重新发送请求。

•固定间隔:在支付失败和重试之间等待固定长的时间。

•递增间隔:客户端在第一次重试时等待较短时间,后面每一次重试时则逐渐增加等待时长。

•指数退避(Exponential Backoff)[插图]:在每次重试失败之后增加重试之间的等待时间。例如,当一个请求第一次失败时,我们在1秒之后重试;如果它第二次也失败了,在重试之前我们等待2秒;如果它第三次仍然失败,我们在重试之前等待4秒。

•取消:客户端可以取消请求。这是当请求总是失败或者重试不太可能成功时的常见操作。

图-14

选定合适的重试策略是有难度的。没有哪个解决方案适合所有场景。一般的做法是,如果网络故障不太可能在短时间内解决,则使用指数退避。激进的重试策略会浪费计算资源并导致服务过载。好的做法是在Retry-After请求头里提供错误码。

重试可能造成的问题是重复支付。我们看下面两个场景。

场景1:顾客快速点击了两次支付按钮。

场景2:PSP成功地处理了支付请求,但是因为网络错误,响应未能到达我们的支付系统,如图-15所示。

图-15

如图-16所示,顾客被PSP A成功收费,但是因为网络错误,我们的支付系统没有收到收费成功的响应。支付系统认为PSP A发生故障,并且用PSP B来重试支付,导致重复收费。

接下来介绍解决这两种重复支付问题的方法。

幂等性

幂等性是确保“至多一次”的关键。根据维基百科上的定义,“幂等性是一种在数学和计算机科学中特定操作所拥有的特性。由于具有此特性,这些操作可以进行多次但结果在第一次操作之后就不会再改变”。从API的角度来看,幂等性意味着客户端可以重复发送同一个请求且产生的结果相同。

对于客户端(网页/移动应用)和服务器之间的通信,幂等键(Idempotency Key)通常是客户端生成的唯一值,它会在一定时间后过期。UUID通常被用作幂等键,并且受到很多科技公司的推荐,比如Stripe和PayPal[插图]。为了执行幂等支付请求,要将幂等键添加到HTTP请求里,HTTP请求头是存放“idempotency-key:<key>”这个键值对的常见地方。

现在我们理解了幂等性的基本概念,让我们看看它如何帮我们解决之前提到的重复支付问题。

如果顾客快速点击了两次付款按钮会怎样?

如图-16所示,当用户第一次点击付款按钮时(第一次请求),会生成一个幂等键,并且它作为HTTP请求的一部分被发给支付系统。

图-16

对于第二个请求,因为支付系统已经见过这个幂等键,所以它会将第二个请求视为重试。如果你在请求头里包含了之前指定的幂等键,支付系统会返回之前请求的最新状态。

如果支付系统检测到拥有相同幂等键的多个并发请求,则只有一个请求会被处理,对于其他请求,支付系统会返回“429 Too Many Requests”这个状态码。

如果支付请求已被成功处理,但是因为网络错误导致返回的响应丢失(如图-15所示),该怎么办?

在这个场景里,我们需要在和外部系统(PSP)交互时维持幂等性,并跟踪支付状态(成功、失败、等待)。图-17给出了一个例子。

1.支付系统首先在交易日志数据库中记录带幂等键的支付请求。

2.支付系统请求PSP A来初始化收款请求。

3.收款请求被PSP A成功执行,但是因为网络错误,本应返回给支付系统的响应丢了。

4.在重试支付之前,支付系统从交易日志数据库中获取之前的支付状态和它之前连上的PSP的信息。

5.通过检查交易日志,支付系统知道支付请求被PSP A处理过,但是系统没收到响应。因此,它使用PSP A重试支付,并在重试请求里包含幂等键。

6.PSP A使用幂等键来确保不会多次收款。它返回之前因为网络错误而失败的响应。7.在交易日志数据库中记录从PSP A返回的响应。

7.在交易日志数据库中记录从PSP A返回的响应。

大部分PSP和银行的服务都以幂等的方式实现。通过在交易日志数据库中保存支付交易的状态,我们总是可以知道在支付生命周期的哪里遇到了问题。

图-17

3.2 同步支付vs.异步支付

支付流程可以是同步的也可以是异步的。这是必须在设计早期做的非常重要的决策。为了做出明智的决策,我们仔细地研究这两个选项。

客户端和服务器之间的通信

在深入研究之前,理解一些术语很重要。

•客户端:可以是移动应用、网站、API请求等。

•服务器:我们的支付系统。客户端和服务器之间的通信可以分为两类。

•同步通信:客户端发送支付请求,然后等待服务器的响应,将连接保持为打开的状态,直到知道交易结果。HTTP就是这样工作的。

•异步通信:客户端不等待服务器的响应,一旦发送了支付请求,连接就关闭了。当请求被处理后,结果被返回给客户端,这通常是通过一个网络钩子(Webhook)[插图]来实现的。Webhook,也称为网络回调,是一个应用/服务提供实时更新给其他应用/服务的方式。

同步的客户端/服务器通信过程如图-18所示。客户端发送HTTP支付请求,然后服务器(支付系统)通过HTTP响应返回结果。

图-18

图-19展示了异步的客户端/服务器通信过程。异步通信的关键组件是队列。在我们的讨论中,队列和Kafka可互换使用。异步通信按如下方式工作。

图-19

1.客户端通常通过HTTP发送支付请求。这会触发一个业务事件,该事件被放入Kafka队列中。

2.异步Worker消费业务事件。

3.一旦业务事件被异步Worker处理完,后端就会发送响应给对应的客户端。

是等待响应还是把请求放入队列以便稍后再处理,这是值得好好斟酌的问题。我们来看几个使用场景。

•当顾客在网上买实体商品时,异步通信会是一个好的选择,因为通常商家需要时间来准备发货。

•如果顾客购买的是可下载的虚拟商品,在客户端和服务器之间使用同步通信是合理的。在允许下载虚拟商品之前,确认支付成功很重要。

•对于基于订阅的数字内容,比如Netflix和Spotify,异步通信可能是一个好选择,我们可以在收到支付成功的确认信息之前授权用户访问。大部分的交易最后都会成功,而顾客不再需要等待支付处理的结果就可以提前观看内容,这会提升用户体验。对于失败的交易,我们的系统会通知顾客,如果款项还是没有准时收到,可能会收回访问权限。

•对于数字钱包应用,如果我们的支付系统知道准确的账户余额,异步通信是一个好主意。

内部服务之间的通信

内部服务可以使用两种通信模式:同步和异步。两者的解释如下。

(1)内部服务之间的同步通信。同步通信,比如HTTP协议,在小规模系统中运作得非常顺畅,但是当业务规模扩大时,问题就变得明显,如图-20所示。这种通信模式创建了一个依赖很多服务的长请求和响应周期。

图-20

这个模式存在以下问题。

•性能低。如果链条中的任何服务性能不佳,整个系统都会受到影响。

•故障隔离差。如果PSP或者任何其他服务出现故障,客户端就不会收到响应。

•耦合度高。请求的发送者需要知道接收者。

•很难扩展。如果没有使用队列来作为缓冲区的话,系统很难扩展以支持突然增长的流量。

(2)内部服务之间的异步通信。

异步通信可以分为以下两类。

•单接收者:每个请求(消息)由一个接收者或者服务处理。这通常通过共享的消息队列来实现。消息队列可以有多个订阅者,但是一旦消息被处理,就会从队列中被移除。我们看一个具体的例子。在图-21中,服务A和B都订阅了共享消息队列。一旦消息m1和m2被服务A和B分别消费,这两个消息都会从队列中被移除,如图-22所示。

图-21

图-22

•多个接收者:每个请求(消息)被多个接收者或者服务处理。这里Kafka很管用。当消费者收到消息时,消息并没有从Kafka中被移除。同一条消息可以被不同的服务处理。这个模型很适合支付系统,这是因为同一个请求可能触发多个操作:发送推送通知、更新财务报表、进行数据分析等。图-23展示了一个例子。同一个支付事件被发布到Kafka上且被不同的服务消费,比如支付系统、数据分析服务、推送通知服务和记账服务。

图-23

一般而言,同步通信在设计上更简单,但它不允许服务自治,并且随着同步依赖的增加,整体的性能表现也会变差。异步通信在设计上可能会牺牲一些简单性和一致性,来换取可扩展性和故障容忍性。对于有复杂业务逻辑和对第三方依赖高的大型支付系统,异步通信是更好的选择。

3.3 一致性

在分布式支付系统中,通常需要多个服务一起来完成一个逻辑上的原子操作。即使在单个服务内部,我们也有可能用到多个数据库、缓存服务器、消息队列等。为了达到可靠性和可用性要求,数据存储服务器通常在多个数据中心之间复制。如果一个服务或者服务器发生了故障,就有可能导致数据不一致。维护数据一致性是有挑战性的,但也非常重要。在本节中,我们会回答一些重要问题:数据不一致是如何发生的?可以用什么技术来减少数据不一致?如何修复不一致的数据?

支付状态机

一个支付交易在它被完全处理完之前会经历不同的状态。图-24展示了信用卡支付交易可能经历的状态。请注意,下面列举的支付状态是针对这个系统设计面试的简化版本,你不需要记住这些概念。想要了解更严谨的定义和分析,你可以在一些主要的信用卡或者PSP的网站上找到这些信息。

•开始:启动一个支付交易。

•授权:支付服务商确保支付方式(信用卡/借记卡)是有效的,并且卡内有足够的资金。

•捕获(Capture):在授权付款之后,需要进行捕获操作。捕获意味着通知信用卡公司,需要付给亚马逊必要金额的资金。

•结算(Clearing):支付交易被记入持卡人的信用卡账户。

•入账(Funding):将资金存入商家账户。

•取消支付(Void):如果你不希望已授权的支付交易被捕获,可以取消支付。取消支付和退款的区别在于一个支付交易是否已结算(完成)。未结算的交易可以被取消。

图-24

•退款:把资金退还给顾客。

•失败:任何支付操作都有可能失败。在图-24中没有显示失败状态,但它是任何支付操作都可能出现的结果。

我们的一个设计目标是保持支付系统内部的状态与PSP中的外部状态一致,如图-25所示。

图-25

我们应该在哪里存储交易状态数据呢?因为关系型数据库提供了ACID的特性,所以我们选择它。数据库事务可以用来有效地确保一致性。

数据库

我们从简单的设计开始,假设所有的数据都存储在单体数据库中。

(1)单体数据库。支付交易意味着把资金从一个账户转移到另一个账户。尽管真正的资金移动发生在PSP内部,但我们的支付系统也需要记录每个交易状态,包括授权、捕获、结算、入账、退款等。

这个单体数据库可以用两张表来建模:transaction和transaction_entry(见图-26、表-1和表-2)。请注意,图-26所示的这两张表里只包含了最重要的参数。

•transaction:记录每个支付交易。

•transaction_entry:存储支付交易的不同状态。一个交易通常包含多个交易条目。
交易条目只能追加写(不可变),原因如下:

第一,我们必须存储完整的交易历史以便审计。

第二,当支付交易出错时,这种方式很容易追溯是哪一个步骤导致了问题。

图-26

表-1

表-2

(2)分布式数据库。在单体数据库中保持数据一致性是相对简单的,但是在分布式数据库中实现数据一致性就很有挑战性了。图-27展示了一个例子。

图-27

在图-27中,幂等数据首先被写入主数据库n1,然后被复制到副本n2和n3。因为存在滞后,副本n3中没有最新的数据。如果客户端从副本n3读数据,获取的数据就是过时的。有三种方法可以解决这个问题。

第一种方法是为了避免副本滞后,只在主数据库上存储幂等数据。读和写的操作都只在主数据库上进行。但是,这个方法有一个明显的问题——缺乏可扩展性。Airbnb通过使用幂等键对数据库分片来解决这个问题。

第二种方法是使用强一致性的模型。强一致性意味着客户端不会看到过时的值。有一点要注意,强一致性通常需要牺牲性能:为了在某个值上达成一致,我们要等待最慢副本的响应。

第三种方法是使用共识协议进行复制。共识是在分布式系统中的一个重要但复杂的话题。它意味着多个服务器对值达成一致。为了对一个值达成共识,大部分节点必须接受提议的值。如第4章讨论的那样,仲裁共识可以对读和写操作都保证一致性。

修复不一致的状态(数据)

为了确保服务之间的一致性,通常使用共识协议,比如两阶段提交(Two-phase Commit)、Raft、Paxos、Saga模式等。当两个不同服务之间的状态产生分歧时,我们希望修复状态,使它们保持一致。有两种方法可以修复不一致的状态:同步修复和异步修复。

•同步修复:通过后续的读/写请求来修复不一致的状态。

•异步修复:使用消费者、定时任务、表扫描等来发现一致性问题,并修复不一致的状态。我们来看一个同步修复的例子,如图-28所示。

对图-28的详细解释如下。

1.客户端向支付系统发送支付请求。

2.因为这是一个新请求,所以将幂等键插入数据库。

3.将支付状态存储在数据库中。

4.支付系统请求外部PSP来向客户端收款。

5.因为网络错误,支付系统没有收到来自外部PSP的响应。

因此,支付系统不确定支付请求是否已成功执行。

图-28

6.对外部请求的超时被触发,支付系统返回一个可重试的错误给客户端。

7.客户端重试支付来修复潜在的不一致状态。因为之前的状态被存储在数据库中,所以我们可以轻松地从之前停止的地方继续。

8.如果PSP API支持幂等性,我们就会使用相同的幂等键来重试支付。

9.HTTP响应被返回给客户端:要么成功,要么失败。如果一个可重试的错误被返回,客户端会再次重试支付。

网络请求可能不可靠。一个好的做法是当数据库事务处于活动状态时不要发送网络请求。很多公司把一个API请求分为3个阶段:预RPC、RPC和RPC后。RPC(Remote Procedure Call,远程过程调用)是一个协议,它使得应用程序可以向远端服务器发送请求。

•按照这种分阶段的方式,数据库交互应当只发生在预RPC和RPC后阶段。

•网络请求应当只发生在RPC阶段。

另一个好的做法是为外部请求设置一个超时时间,这样系统就不会在外部发生故障时一直等待。

我们来看一个异步修复的例子。如图-29所示,指定的服务器在运行一致性检查的任务。如果检测到任何不一致,异步修复消费者会尝试修复。这个过程也叫作支付对账(Payment Reconciliation),通常是通过匹配内部数据(交易日志)和外部数据(PSP)来实现的。

图-29

3.4 处理支付失败

在分布式系统中,故障不仅无法避免而且很常见。每个支付系统都必须处理失败的交易。可靠性和容错性是支付系统的关键要求。我们来看几个应对这些挑战的技术:持久化保存支付“状态”、重试队列和死信队列(Dead Letter Queue)。

持久化保持支付“状态”

在支付周期的任何阶段都拥有明确的支付“状态”是至关重要的。这样,当发生故障时,我们就可以确定支付交易现在的“状态”,并决定是否需要重试或者退款。支付“状态”可以持久化存储在一个只可追加写的数据库表中。

重试队列和死信队

列为了优雅地应对失败,我们可以使用重试队列和死信队列,如图-30所示。•重试队列:可重试的错误,比如暂时性错误,都被路由到重试队列。•死信队列:如果一个消息一次又一次地失败,它最终就会被放入死信队列中。死信队列很有用,可以调试和隔离有问题的消息,以便手动对其进行检查,确定为什么它们没有被成功处理。

图-30

对图-30的解释如下。

1.对于可重试的失败交易,事件被路由到重试队列。

2.对于不可重试的失败,比如不符合规定的输入,会将错误存储到数据库里。

3.支付消费者从重试队列里拉取事件。

4.支付消费者请求支付系统来执行支付交易。

5a.如果支付交易失败且重试次数没有超过阈值,事件就被路由至重试队列。

5b.如果支付交易失败且重试次数超过阈值,事件就被路由至死信队列。这些事件可能需要手动检查。

我们需要回答的一个重要问题是:如何对支付消费者实现幂等性。

•给每个事件指派唯一的ID。

•事件在被处理之前存储在持久化存储里。

•唯一的ID用来确保事件只被处理一次。

3.5 支付安全

支付安全正在变成最严重的问题之一。我们总结了一些用来应对网络攻击和信用卡失窃的技术,如表-3所示。

4 .总结

在本文中,我们讨论了收款流程、付款流程和实时卖家仪表板,深入讨论了重试、幂等性和一致性的话题。在本文的最后,我们还探讨了对支付错误的处理和支付安全。支付系统是极度复杂的。尽管我们已经探讨了很多话题,但是依然有很多其他值得讨论的话题未涉及。下面列出了一些有代表性的(而非所有的)有趣话题。

•监控。监控关键指标是现代应用至关重要的部分。通过广泛的监控,我们可以回答像“特定支付方法的平均接受度如何?”“服务器CPU的使用率是多少?”这样的问题。我们可以在仪表板上创建和展示这些指标。

•告警。当异常事件发生的时候,向值班开发人员发出警报,以便其快速响应,这一点很重要。

•调试工具。“支付为什么会失败”是一个经常会被问到的问题。为了让开发人员和客户支持人员进行调试时更容易,开发一些工具是很重要的,因为人们通过这些工具可以直观地看到支付交易的交易状态、处理服务器、PSP等信息。

•自动扩展。如果你的业务要处理数百万甚至十亿级的交易,你会希望你的数据库层、API层、缓存层等能够自动扩展。•货币兑换。货币兑换是设计面向国际用户的支付系统时要考虑的重要因素。

•地理位置。在不同地区,可能有完全不同的支付方式。

•现金支付。在印度、巴西和一些东南亚国家,现金支付非常普遍。设计支付系统时,我们必须考虑现金支付。

•与Google/Apple Pay集成。


http://www.kler.cn/a/454309.html

相关文章:

  • Flink CDC MySQL 同步数据到 Kafka实践中可能遇到的问题
  • 详细对比JS中XMLHttpRequest和fetch的使用
  • python中os.path.isdir()问题
  • linux---awk命令详细教程
  • NLP 中文拼写检测纠正论文 C-LLM Learn to CSC Errors Character by Character
  • 生成10级子目录,每个子目录下有100个不同大小的文件
  • UnoCSS 的作用与特点
  • idea配置gitee仓库
  • 讯飞语音听写WebApi(流式)【React Native版】
  • 报警推送消息升级的名厨亮灶开源了。
  • 【Django篇】--动手实践Django基础知识
  • 《Go 语言变量》
  • C语言学习笔记(1)
  • 游戏引擎学习第62天
  • Maven核心概念总结
  • Blender高效优化工作流程快捷小功能插件 Haggis Tools V1.1.5
  • jvm排查问题-实践追踪问题 与思路--堆内堆外内存泄漏排查方针
  • HarmonyOS NEXT 实战之元服务:静态案例效果---咖啡制作实况窗
  • css
  • 随时随地编码,高效算法学习工具—E时代IDE
  • PDF书籍《手写调用链监控APM系统-Java版》第10章 插件与链路的结合:SpringBoot环境插件获取应用名
  • Uniapp 微信小程序检测新版本并更新
  • 数据分析的常见问题及解决方案
  • 安全合规遇 AI 强援:深度驱动行业发展新引擎 | 倍孜网络CEO聂子尧出席ICT深度观察报告会!
  • C++-----------映射
  • Java Spring Boot 项目中嵌入前端静态资源:完整教程与实战案例