RESTful API和事件驱动系统中的幂等性

如果您正在构建 REST API 或事件驱动系统,幂等性是您需要考虑的一个非常重要的属性,因为它对于拥有弹性系统至关重要,并且它将帮助您避免不必要的额外副作用。

您的借记卡是否曾因同一笔交易被扣款两次?或者您是否从网上商店订购了一件商品,但收到了两次?原因可能是你在那些系统中执行的操作不是我能控制的!

在本文中,我们将介绍您需要了解的有关RESTful API 和事件驱动系统背景下幂等性的所有重要内容,以及它对于构建弹性系统和解决方案的重要性。

什么是幂等性?
幂等性的概念存在于数学和计算机科学中,它是某些操作的一个属性,简单地说就是多次执行该操作的结果与仅执行该操作一次的结果相同,我们将此类操作描述为幂等。

计算机科学中自然幂等操作的一个著名示例是 Set 数据结构上的添加操作。原因很简单,因为无论将某个项目添加到 Set 中多少次,结果始终是相同的。

banq注:幂等性就是重复 提交,判重问题。

弹性系统
要充分理解幂等性的重要性,先决条件是充分理解系统弹性的概念,这是任何体面的现代系统必须具备的品质。

以下说法是我从ChatGPT得到的系统弹性的定义:
计算机科学中的系统弹性是指计算机系统或互连系统网络承受各种故障、中断或不良事件并从中恢复的能力。它涉及设计、实施和管理计算机系统,即使面对意外的挑战,也能确保其可用性、可靠性和性能。

因此,基本上,弹性系统的设计方式使其能够从不同类型的意外故障和各种事件中恢复,同时仍然提供所需和预期的服务和功能。

其中,拥有弹性系统的最重要策略之一是实施重试机制,这是一种简单非常有效的从故障中恢复的方法。

但是如果我们的系统不能正确处理重复或重试的请求(即幂等性),那么实现重试机制就不安全;否则,我们肯定会产生意想不到的额外副作用和不想要的结果。

在下面的部分中,我们将更仔细地研究幂等性的重要性,特别是在 REST API 和事件驱动系统的背景下。

REST API 中的幂等性
当我们设计API时,我们必须意识到,在很多情况下,我们可能会收到来自客户端的重复请求,无论是有意的(例如,超时或网络问题后重试)还是无意的(例如,客户端逻辑中的错误、多次意外点击)。因此,我们在设计 API 时始终必须考虑幂等性,这样我们才能拥有一个有弹性的系统并避免意外的副作用和结果。

作为一个现实世界的示例,假设我们有一个电子商务网站,其中包含一个“立即订购”按钮,客户单击该按钮即可快速下订单。如果出于某种原因,客户意外地多次点击按钮,并且我们的 API 不是幂等的,那么我们最终将为客户下两个订单,而不是只下一个预期订单。

HTTP 方法幂等性
在 REST API 的上下文中,我们讨论不同 HTTP 方法的幂等性,如果使用该方法的多个相同请求对服务器的预期效果与单个请求的效果相同,则 HTTP 方法是幂等的。
如果 REST 原则正确实现并且根据HTTP 规范,我们有以下内容:

  • 安全 HTTP 方法(GET、OPTIONS、HEAD 和 TRACE)是幂等的,因为它们仅供只读使用,并且不应更改服务器的状态。
  • PUT和DELETE也是幂等的,因为它们通常需要服务器上资源的唯一标识符;因此,如果多次请求,预期效果将是相同的。例如,在 DELETE 的情况下,第一个请求很可能会返回200,而如果资源在服务器上被有效删除(硬删除),后续调用可能会返回404,但如前所述,这并不重要,因为对服务器的预期效果是相同的。
  • POST方法不是幂等的,因为如果请求没有唯一标识符,将为每个请求创建一个新资源,并且大多数情况下都是这种情况,因为标识符的创建被委托给接收系统。
  • PATCH也不是幂等的,这可能会令人困惑,因为对于 PUT,PATCH 端点也使用资源标识符,但这里的主要区别在于,使用 PATCH,我们对资源进行部分更新,并且有多种可能的方法来执行此操作,并且请求是否幂等取决于所选的实现,而使用 PUT,我们总是更新整个资源。

实现幂等 REST API
实现幂等 API 的第一步是为每个请求拥有唯一的标识符,有时这可以是自然标识符(也称为业务标识符),或者像我们之前的例子一样,我们需要让客户端为每个请求创建唯一标识符(幂等密钥),并确保在重试时使用相同的密钥,以便我们可以正确处理重复的请求。

收到消费者的请求后,我们的 API 需要跟踪接收到的幂等性密钥以及数据存储(例如键值数据库)中相应请求的状态。除此之外,我们可能还想保存其他信息,例如最终响应(如果它已经提供给客户端),这样如果再次发出相同的请求,我们可以将其返回给客户端。

在每次向服务器发出请求时,表或集合上相应的幂等键行或条目都应该被锁定,如果再次收到相同的请求,我们需要检查前一个请求的状态来决定如何响应客户端,即如果前一个请求成功,我们可能希望返回存储的响应;否则,如果失败,我们应该检查失败的性质,以决定请求是否可以重试或需要特殊处理。

此外,锁的过期时间也很重要,以便能够从响应无法持久或服务器超时的情况中恢复。

当涉及到保存的幂等性密钥时,无限期地保留它们可能不是一个好主意,因为它们的大小会迅速爆炸,并且可以合理地假设一段时间后,将不再重试。因此,根据 API 的要求定义此数据的保留策略非常重要。

需要注意的是,到目前为止我们提出的只是实现 API 幂等的基本思想,但根据您的系统架构和需求(例如,事务、调用多个下游系统或服务等),实现可能会有所不同,并且或多或少复杂。

在某些情况下,拥有幂等 API 确实至关重要(例如,银行业务),这就是为什么 Airbnb(如本文中所述)创建了Orpheus,这是一个用于维护幂等系统的幂等库,以避免双重支付问题。

事件驱动系统中的幂等性
如今,许多系统都基于事件驱动的架构,该架构本身通常依赖于事件路由器或消息代理。公共云中这些消息代理的一些示例是AWS SQS 或 GCP Pub/Sub,通常,这些解决方案提供所谓的至少一次传递保证,确保消息至少到达其使用者一次。

至少一次交付保证是构建弹性事件驱动系统的重要基础,但它要求消费者能够处理重复事件,而不会产生额外或不需要的副作用(即幂等消费者)。

为了实现幂等消费者,事件还需要具有唯一标识符,可以是自然标识符或事件生产者生成的消息标识符(幂等键)。然后,消费者可以在数据存储上跟踪接收到的事件,其方式与我们之前针对 REST API 介绍的方式相同。

对于幂等性令牌或密钥,UUID通常是一个不错的选择,因为它们在很大程度上保证了唯一性,并且我们可能会遇到它们的不同名称(例如idempotency_key、deduplication_id等),但最终的目的是相同的。