百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

在 Postgres 中实现类似 Stripe 的幂等键

toyiye 2024-06-21 12:23 11 浏览 0 评论

更多互联网精彩资讯、工作效率提升关注【飞鱼在浪屿】(日更新)


API 中幂等性是一个很大的概念。幂等节点是可以被调用任意次数的节点,同时保证请求只会被处理一次。在复杂的现实世界中,客户端和服务器可能偶尔崩溃或在请求中途断开连接,幂等性让系统更健壮以应对故障。不确定请求是成功还是失败的客户端可以简单地继续重试,直到得到明确的响应。

正如我们将在本文中看到的那样,实现一个服务器以使其所有请求都完全幂等并不总是那么容易。对于仅在 ACID 数据库中改变局部状态的节点,可以通过将请求映射到事务来获得健壮且简单的幂等性实现。这种方法比这里描述的要容易得多,也没有那么复杂,建议任何能摆脱复杂的人都走这条路。


密钥的幂等性

需要在外部状态(即在本地 ACID 存储之外)进行同步更改的实现设计起来有些困难。这方面的一个基本示例是,如果应用程序需要发出 Stripe 请求(出名的支付机构,https://stripe.com/zh-cn-hk)来创建费用,并且需要在Stripe内部知道它是否被处理,以便它可以决定是否提供某些商品或服务。为了保证此类端点的幂等性,引入幂等键的概念

幂等键是由客户端生成并与请求一起发送到 API 的唯一值。服务器存储用于在其末尾记录该请求状态的密钥。如果请求中途失败,客户端会使用相同的幂等键值重试,服务器使用它来查找请求的状态并从中断处继续。“幂等键”这个名字来自于 Stripe 的 API。

传输幂等密钥的常用方法是通过 HTTP 头:

POST /v1/charges

...
Idempotency-Key: 0ccb7813-e63d-4377-93c5-476cb93038f3
...

amount=1000¤cy=usd

一旦服务器收到请求,然后处理成功或失败,它就会存储请求的最终处理结果并将它们与幂等键相关联。如果客户端使用相同的密钥发出另一个请求,服务器只需短路并返回存储的结果。

密钥并不打算用作永久的请求存档,而是用作确保近期正确性的机制。服务器应该将它们从系统中回收,超出它们不会有太大用处的范围——比如 24 小时左右。


Rocket Rides应用程序

接着看Rocket Rides应用程序如何为 API 设计幂等键。

Stripe 团队构建了一个名为Rocket Rides的应用程序,以演示 Connect 平台和 API 其他有趣部分的使用。在 Rocket Rides 中,赶时间的用户与飞行员共享旅行数据以快速到达目的地 。

Rocket Rides 应用程序

该app部署在一台简单的服务器上执行,但软件往往随着时间而请求增长,因此更具有代表性的用户和车主实时服务会是什么样子,接下来将对复杂的事情做一些点缀。


请求生命周期

当请求进来时,执行以下操作:

  1. 插入幂等键记录。
  2. 创建骑行记录以跟踪即将发生的骑行。
  3. 创建行程关联的审核记录。
  4. 对 Stripe 进行 API 调用以向用户收取费用(这里保留自己的堆栈,这会带来一些风险)。
  5. 使用创建的费用 ID 更新行程记录。
  6. 通过电子邮件向用户发送收据。
  7. 用结果更新幂等键。

Rocket Rides 后端的典型 API 请求

后端实现使用幂等键从Rocket Rides 移动应用程序调用。如果请求失败,应用程序将继续使用相同的密钥重试操作,后端实现者的工作是确保这是安全的。向用户的信用卡收费,我们但绝对不能两次收费。


生产阶段

大多数情况下,可以期待每个 Rocket Rides API 调用都能顺利进行,并且每个操作都会成功而不会出现问题。然而,当达到每天数千次 API 调用的规模时,开始注意到这里和那里出现的一些问题;由于蜂窝连接不良导致请求失败,对 Stripe 的 API 调用偶尔失败,或者由于以超音速移动严重湍流周期性地使飞行员离线。在达到每天数百万次 API 调用的规模后,基本概率将决定将一直看到此类事情发生。

看几个可能出错的例子:

  • 由于违反约束或数据库连接问题,插入幂等键或骑行记录可能会失败。
  • 调用 Stripe可能会超时,不清楚=收费是否成功。
  • 联系 Mailgun 发送收据可能会失败,给用户留下信用卡费用但没有正式的交易通知。
  • 客户端可能会在向服务器传输请求时断开连接,从而在中途取消操作。

让我们介绍一些可以优雅地解决这个问题的想法。

外部状态变更

要确定在哪里进行外来状态变更;也就是说,调用和操作另一个系统上的数据。比如 Stripe 上产生收费费用、添加 DNS 记录或发送电子邮件。

一些外部状态变更本质上是幂等的(例如添加 DNS 记录),一些不是幂等的,但可以在幂等密钥的帮助下使其成为幂等的(例如在 Stripe 上收费,发送电子邮件),并且一些操作不是幂等的,最常见的是因为外部服务没有以这种方式设计它们并且没有提供像幂等键这样的机制。

本地与外部的区别很重要的原因是,与本地操作不同,本地可以利用 ACID 存储来回滚不对的结果,一旦进行了第一个外部状态突变,我们就会保持另一种承诺。已经将数据推送到超出我们自己边界的系统中,而且应该记录它。


任意两个系统之间

这里对 Stripe 的 API 调用作为一个常见示例,但即使是基础架构中的外部调用也是这个道理!将向 Kafka 发送消息被视为原子操作,因为它们具有如此高的成功率,以至于他们觉得自己是。它们不是,并且应该像任何其他易出错的外部变更一样对待。


原子阶段

一个原子阶段是一组发生在当地的交易状态突变之间的外围变更。我们说它们是原子的,因为可以使用像 Postgres 这样的 ACID 兼容数据库来保证它们要么全部发生,要么都不发生。

启动任何外来状态变更之前,应该安全地提交原子相。如果调用失败,本地状态仍然会有它发生的记录,我们可以用它来重试操作。


恢复点

恢复点或检查点,我们到达后已经成功执行任何原子阶段的点外部突变。它的目的是允许正在重试的请求跳回到生命周期中最后一次尝试失败之前的那个点。

为方便起见,把到达的恢复点的名称存储到将建立的幂等键上。所有请求最初都将获得一个恢复点started,并且在任何请求完成后(同样,通过成功或确定性错误)它将被分配一个恢复点finished。当处于原子阶段时,应将向新恢复点的转换作为该阶段事务的一部分提交。


后台作业和暂存

带内外部状态变更使请求变慢且更难以推理,因此应尽可能避免使用它们。在许多情况下,可以通过将请求发送到后台作业队列,将这种类型的工作推迟到请求完成之后。

在Rocket Rides 示例中,无法推迟对 Stripe 的收费 。因为我们想知道它是否立即成功,以便我们可以在没有成功的情况下拒绝该请求。

通过使用事务分阶段的工作分流,我们可以使用工作线程隐藏具体工作,直到我们通过在事务中隔离他们来确认他们已准备好工作。这也意味着后台工作成为原子阶段的一部分,并大大简化了其操作属性。工作应该总是尽可能地卸载到后台队列。


星际旅行之旅

现在已经介绍了一些关键概念,我们准备支持 Rocket Rides,使其能够抵御任何可以想象的故障。将基本模式放在一起,生命周期分解为原子阶段,并组装一个可以从故障中恢复的简单实现。

Atomic Rocket Rides 存储库中提供了所有这些的工作版本(带测试)。下载该代码并继续操作可能会更容易。

git clone https://github.com/brandur/rocket-rides-atomic.git

幂等键关系

让我们为应用程序中的幂等键设计一个 Postgres 模式:

这里有几个值得注意的属性:

  • idempotency_key:这是用户指定的幂等键。发送诸如 UUID 之类的具有随机性的内容是一种很好的做法,但不一定是必需的。限制了字段长度,以便没有人发送任何过于奇特的东西。idempotency_key是唯一的但跨度的, (user_id, idempotency_key)因此只要跨不同的用户帐户,就可以对不同的请求使用相同的幂等密钥。
  • locked_at:指示此幂等密钥是否正在工作的字段。创建密钥的第一个 API 请求将自动锁定它,但随后的重试也将设置它以确保它们是执行工作的唯一请求。
  • params: 请求的输入参数。这主要是为了在用户发送两个具有相同幂等键但参数不同的请求时发生错误,但也可以用于我们自己的后端以将未完成的请求推送到完成(。
  • recovery_point:为幂等请求完成的最后一个阶段的文本标签(参见上面的恢复点)。获取 的初始值started并在认为完成处理请求时设置为finished

其他架构

回想一下 Rocket Rides 的目标 API 生命周期。

Rocket Rides 后端的典型 API 请求。

构建此应用程序所需的所有其他内容(包括审计记录、乘车和用户)。目标是最大限度地提高可靠性,我们将尝试遵循数据库最佳实践NOT NULL,并尽可能使用、唯一和外键约束。


设计原子阶段

现在我们已经了解了数据应该是什么样子,将 API 请求分解为不同的原子阶段。这些是识别它们的基本规则:

  1. 更新幂等键会得到它自己的原子阶段。
  2. 每个外来状态变更都有自己的原子阶段。
  3. 在确定这些阶段之后,它们之间的所有其他操作都被分组为原子阶段。即使在两个外部状态突变之间对 ACID 数据库进行了 100 次操作,它们都可以安全地属于同一阶段。

因此,在示例中,我们有一个原子阶段插入幂等键 ( tx1),另一个原子阶段调用 Stripe ( tx3) 并存储结果。周围的所有tx1tx3的其他操作组合在一起并成为两个阶段,tx2tx4可分别通过由一款之前提交的交易设定的恢复点达(startedride_created,和charge_created)。

对 Rocket Rides 的 API 请求分为外部状态变更和原子阶段。


原子阶段实现

我们对原子阶段的实现将把所有东西都包装在一个事务块中(这里使用的是 Ruby,但同样的概念在任何语言中都是可能的),并为每个阶段提供三个可以返回的选项:

  1. RecoveryPoint设置新的恢复点。这与阶段的其余部分发生在同一个事务中,所以它都保证是原子的。正常执行进入下一阶段。
  2. Response将幂等请求的恢复点设置finished为并向用户返回响应。这应该用作正常成功条件的一部分,但也可用于在出现不可恢复的错误时提前返回。例如,假设用户的信用卡无效——无论请求重试多少次,它都不会通过。
  3. NoOp指示程序流应继续,但不应设置恢复点和响应。

在序列化错误的情况下,返回409 Conflict意味着并发请求与我们试图做的事情发生冲突。在实际应用中,可能只想立即重试该操作,因为这次很有可能成功。

对于其他错误,返回500 Internal Server Error. 对于任一类型的错误,我们尝试在完成之前解锁幂等密钥,以便另一个请求有机会重试它。


幂等键更新插入

当新的幂等键值进入 API 时,我们将创建或更新相应的行,用于跟踪其进度。

最简单的情况是以前从未有过密钥。只需插入一个具有适当值的新行。

如果我们有了密钥,请将其锁定,以免其他可能同时运行的请求也尝试该操作。如果密钥已被锁定,则返回 409 Conflict以向用户表明这一点。

一个已经设置为的键finished被简单地允许失败并在标准成功路径上返回响应。

乍一看,这段代码可能看起来不像让两个并发请求紧接着进入并尝试锁定同一个键是安全的,但这是因为原子阶段包含在SERIALIZABLE 事务中。如果两个不同的事务都试图锁定任何一个密钥,则其中一个将被 Postgres 中止。


有向非循环状态机

把 API 请求的其余部分实现为一个简单的状态机,其状态是一个有向无环图 (DAG)。与普通图不同,有向无环图只向一个方向移动,并且永远不会返回自身。

每个原子阶段都将从恢复点激活,该恢复点要么从恢复的幂等密钥中读取,要么由前一个原子阶段设置。我们继续通过各个阶段直到达到一个finished状态,在该状态下循环被打破并将响应发送回用户。

已经完成的幂等密钥将进入循环,立即中断,并将存储在其上的任何响应发回。


初始簿记

第二阶段(上图中的tx2)很简单:在本地数据库中为骑行创建一条记录,插入一条审计记录,并将新的恢复点设置为 ride_created



调用Stripe

有了基本的数据结构,是时候尝试通过 Stripe 向客户收费来尝试外部状态变更了。在这里,我们使用已存储在其用户记录中的 Stripe 客户 ID 收取 20 美元的费用。成功后,使用新的 Stripe 费用 ID 更新上一步中创建的行程并设置恢复点 charge_created

对 Stripe 的调用多了一些不可恢复错误的可能性(即无论重试多少次都不会看到调用成功的错误)。如果遇到,将请求设置为finished并返回适当的响应。如果信用卡无效或交易被支付网关拒绝,则可能会发生这种情况。


发送收据并完成

现在费用已经被保存,下一步是向用户发送收据。进行外部邮件调用通常需要它自己的外部状态突变,但因为使用的是事务性阶段的作业消耗,可以保证操作与事务的其余部分一起提交。

最后一步是设置一个响应,告诉用户一切都按预期进行。我们完成了!


其他事项

除了网络运行过程的API,需要别的角色使一切工作。

入队者

应该有一个入队器staged_jobs在提交插入事务后将作业从作业队列移到 作业队列。

完成者

此实现的一个问题是我们依赖客户端将不确定的请求(例如,可能看起来是超时的请求)推送到完成。通常客户愿意这样做是因为他们希望看到他们的请求得到通过,但也有可能出现这样的情况:客户开始工作,永远不会完成,然后永远掉线。

一个延伸目标是实现一个完成者。它唯一的工作是找到看起来从未令人满意地完成并且看起来客户已经放弃的请求,并推动完成。

它甚至不需要特别了解堆栈是如何实现的。它只需要知道如何读取幂等密钥并拥有一个专门的内部身份验证路径,允许它重试任何人的请求。


清理者

幂等性密钥旨在充当保证幂等性的机制,而不是作为历史请求的永久存档。一段时间后, 清理者进程应该删除密钥。

建议设置一个大约 72 小时的范围,这样即使在周五部署了一个错误,导致大量有效请求出错,应用程序仍然可以在整个周末和周一保留它们的记录,开发人员将有一个提交修复并让完成者推动他们成功的机会。

理想的清理者甚至可能会注意到无法成功完成的请求并尝试对它们进行一些清理。如果清理很困难或不可能,它应该将它们放在一个列表中,以便人们可以找出失败的原因。


墨菲定律

现在我们已经准备好了所有的部分,让我们假设墨菲定律的真实性,并想象一些在客户端应用程序与新的Atomic Rocket Rides后端通信时可能出错的场景:

  • 客户端发出请求,但连接在到达后端之前就中断了:使用幂等密钥的客户端知道重试是安全的,因此会重试。下一次尝试成功。
  • 两个请求同时尝试创建幂等键:UNIQUE数据库中的约束保证只有一个请求可以成功。一个通过,另一个得到一个409 Conflict
  • 创建了幂等密钥,但数据库出现故障并很快失败:客户端继续对 API 进行重试,直到它重新联机。完成后,将恢复创建的密钥并继续请求。
  • Stripe 关闭:包含 Stripe 请求的原子阶段失败,API 响应错误,告知客户端重试。他们会继续这样做,直到 Stripe 重新上线并且充电成功。
  • 服务器进程在等待来自 Stripe 的响应时意外结束:幸运的是,对 Stripe 的调用也是使用其自己的幂等键进行的。客户端重试并使用相同的键调用对 Stripe 的新调用。Stripe 自身的幂等性保证确保我们没有向用户收取双重费用。
  • 一个糟糕的部署 500 秒所有请求都中途: 开发人员争先恐后地部署修复程序。结束后,客户端重试,原始请求沿着新的无错误路径成功。如果修复需要很长时间才能完成,以至于客户早已离开,那么完成程序会推动他们通过。

对故障安全设计的关注得到了回报——尽管可能出现各种各样的故障,但系统仍然是安全的。


非幂等外来状态变更

如果我们知道外部状态突变是幂等操作或者它支持幂等键(如 Stripe 所做的那样),我们就知道可以安全地重试我们看到的任何失败。

不幸的是,并非每项服务都会做出这种保证。如果我们尝试进行非幂等的外部状态变更并且失败,我们可能不得不将此操作作为永久错误进行持久化。在很多情况下,我们不知道重试是否安全,我们将不得不采取保守的路线并最终结果是操作失败。

例外情况是,如果我们从非幂等 API 返回错误,但明确告诉我们可以重试。诸如连接重置或超时之类的不确定错误必须标记为失败。

这就是为什么您应该在所有服务上实施幂等性和/或幂等性密钥的原因!


非 ACID 数据存储

值得一提的是,在像 MongoDB 这样的非 ACID 存储中,这一切都是不可能的。没有事务语义,数据库永远无法保证任何两个操作都以原子方式提交——对数据库的每个操作都等同于外部状态突变,因为原子阶段的概念是不可能的。

超越 API

本文主要关注 API,同样的技术也可重用于其他软件。Web 应用程序中的一个常见问题是双重表单提交。用户快速连续两次点击“提交”按钮可能会启动两个单独的 HTTP 调用,并且在提交具有非幂等作用(例如向用户收费)的情况下,这是一个问题。

在最初渲染表单时,可以使用<input type="hidden">添加到包含幂等键的元素。该值将在多次提交中保持不变,服务器可以使用它来删除请求。


培养被动安全

API 后端应该以被动安全为目标——无论遇到什么样的故障,它们最终都会处于稳定状态,即使在最极端的情况下,用户也不会被破坏。从那里,主动机制可以推动系统走向完美的凝聚力。理想情况下,人类操作员永远不必干预来解决问题(或至少尽可能少地干预)。

此处描述的纯幂等交易和具有原子阶段的幂等密钥是朝该方向发展的两种方式。故障不仅被理解为可能发生,而且是预料之中的,并且系统的设计已经考虑了足够多的思想,我们知道无论发生什么,它都会完全容忍故障。

有一个注意事项,即可以在一个系统和所有其他执行外来状态突变的系统之间实现两阶段提交。这将允许分布式回滚,但实现起来非常复杂和耗时,这在实际软件环境中很少见。

相关推荐

「linux专栏」top命令用法详解,再也不怕看不懂top了

在linux系统中,我们经常使用到的一个命令就是top,它主要是用来显示系统运行中所有的进程和进程对应资源的使用等信息,所有的用户都可以使用top命令。top命令内容量丰富,可令使用者头疼的是无法全部...

Linux 中借助 perf 对 php 程序模拟CPU高的案例分析

导语本文是一篇Linux借助工具分析CPU高的优化案例,没有任何干货内容,很详细的展示了优化CPU高的具体步骤,非常适合初中级读者阅读!...

centos漏洞处理方法(centos podman)

centos服务器最近有诸多漏洞,修复命令及对应的漏洞整理后,分享给大家RHSA-2020:1176-低危:avahi安全更新yumupdateavahi-libsRHSA-2017:326...

Linux上的free命令详解(Buffer和Cache)

解释一下Linux上free命令的输出。下面是free的运行结果,一共有4行。为了方便说明,我加上了列号。这样可以把free的输出看成一个二维数组FO(FreeOutput)。例如:FO[2][1]...

linux 命令行之你真的会用吗?--free 基本用法篇

free命令行统计内存使用率及swap交换分区的使用率数据。是由sourceforge负责维护的,在ubuntu上其包名为procps,这个源码包中,除了free还有ps,top,vmstat,ki...

kong api gateway 初体验(konga github)

kongapigateway初体验(firstsight?)。Kong是一个可扩展的开源API层(也称为API网关或API中间件)。Kong运行在任何RESTfulAPI的前面,并通过插件...

在Ubuntu下开启IP转发的方法(ubuntu20 ip)

IP地址分为公有ip地址和私有ip地址,PublicAddress是由INIC(internetnetworkinformationcenter)负责的,这些IP地址分配给了注册并向INIC提...

基于 Kubernetes 的 Serverless PaaS 稳定性建设万字总结

作者:许成铭(竞霄)数字经济的今天,云计算俨然已经作为基础设施融入到人们的日常生活中,稳定性作为云产品的基本要求,研发人员的技术底线,其不仅仅是文档里承诺的几个九的SLA数字,更是与客户切身利益乃...

跟老韩学Ubuntu Linux系列-sysctl 帮助文档

sysctl一般用于基于内核级别的系统调优,man帮助手册如下。...

如何在 Linux/Unix/Windows 中发现隐藏的进程和端口

unhide是一个小巧的网络取证工具,能够发现那些借助rootkit、LKM及其它技术隐藏的进程和TCP/UDP端口。这个工具在Linux、UNIX类、MS-Windows等操作系统下都...

跟老韩学Ubuntu Server 2204-Linux性能管理-uptime指令帮助手册

uptime指令是每个从事Linux系统工作的相关同学必知必会的指令之一,如下是uptime指令的帮助手册。UPTIME(1)...

Openwrt+Rclone+emby+KODI搭建完美家庭影音服务器

特别声明:本篇内容参考了波仔分享,在此表示感谢!上一篇《Openwrt+emby+KODI搭建家庭影音服务器》只适用影音下载到本地的情形,不能播放云盘中的影音,内容较少,缺少了趣味性,也不直观。...

Linux Shell脚本经典案例(linux shell脚本例子)

编写Shell过程中注意事项:开头加解释器:#!/bin/bash语法缩进,使用四个空格;多加注释说明。命名建议规则:变量名大写、局部变量小写,函数名小写,名字体现出实际作用。默认变量是全局的,在函数...

解决 Linux 性能瓶颈的黄金 60 秒

如果你的Linux服务器突然负载暴增,告警短信快发爆你的手机,如何在最短时间内找出Linux性能问题所在?来看Netflix性能工程团队的这篇博文,看它们通过十条命令在一分钟内对机器性能问题进行诊断。...

跟老韩学Ubuntu Server 2204-Linux性能管理-vmstat指令帮助手册

vmstat可查看ubuntlinux的综合性能,是每个从事Linux人员必知必会、需掌握的核心指令之一。vmstat指令帮助手册如下。VMSTAT(8)...

取消回复欢迎 发表评论:

请填写验证码