在讨论接口支持幂等性时,我们往往默认它"相同参数经过多次调用后对系统结果的一致性"。然而,在我最近的工程中,这个概念似乎并不完全正确。
以交易系统中的下单为例,用户下单购买商品,系统需要扣款。付款方式有1. 用户自己的积分资产 2. 从三方支付系统(比如支付宝)扣钱然后发货,用户收货成功后,给用户再发奖励积分。
这里涉及到多个幂等场景,例如:
-
用户点击下单,防止用户重复下单,下单系统要支持幂等。
-
下单系统调积分系统,防止两边系统不一致,重试造成多扣,积分系统需要支持幂等。
-
下单系统调用三方支付系统,防止两边系统不一致,重试造成多扣,积分系统需要支持幂等。
-
用户确认收货后,给用户发放奖励积分。一笔交易发放一次奖励积分,不能多发。
为了专注问题讨论,现在我们来看积分系统怎么支持幂等性。接口支持幂等时,指的是使用相同参数无论调用多少次,对系统造成的结果是一致的。这里面包含两层含义
-
接口调用方来定义相同请求参数
-
接口提供方对相同请求参数, 实现相同的结果。
一个接口的请求参数通常很多,为了简化相同参数的定义,通常约定一个幂等号,即多次请求中的幂等号一致,接口提供方认为是同一请求。
接口实现方的主要操作
增加幂等号查询记录判断来避免重复请求,当然我们还会增加锁控制以及数据库层面的幂等号唯一性设计,来确保幂等号的 不重复。上述方案看似解决了问题。在实际应用中,仅在正常情况下接口返回了正确的结果,并不意味着所有异常场景下都不会出现问题,看下以下特殊场景
场景 | 接口提供方 | 接口提供方返回结果 | 调用方收到的结果 | 是否需要重试 |
---|---|---|---|---|
1 | 操作成功 | 成功 | 成功 | 不需要 |
2 | 操作成功 | 成功 | 失败(网络异常导致超时) | 需要 |
3 | 操作失败(业务异常,比如积分不足) | 失败(业务异常) | 失败 | 不需要 |
4 | 系统异常 | 失败 | 失败 | 需要 |
5 | 奖励积分操作失败(当日限额) | 失败 | 失败 | 不需要 |
场景3
因为是业务异常,重试是徒劳的,所以调用方不需要重试 。
但是调用方不讲武德,仍旧重试。比如第一次返回的是积分不足 。但是隔了一段时间,调用方还来调用,这时候用户积分够了,按照上述流程,这次会重试成功,显然这接口满足不了幂等性。
所以我们在第一次处理时,必须要把处理结果记录下,即使并没有发生真实扣减。这地方的迷惑性在于幂等号,使用幂等号来代替使用相同参数,但是不同时间,相同幂等号对应的服务提供方在不同时刻的资源是不一致的,这些资源也可以理解为入参。所以要以首次请求为准。经过调整后,我们实现了幂等。
但是现在遇到场景5
用户下单成功后,奖励积分给用户。这个在业务上是一个必须达成的操作。但是调用积分系统时,触发了当日限额, 无法发放成功。从积分系统的接口设计角度来说,会一直幂等返回失败。但是下单发放积分奖励的系统不这样认为,他认为这是一个必须达成的操作, 所以会一直重试,这时候即使当日限额提升了,接口因为幂等原因,也无法发放成功。
所以调用方必须要换用幂等号,才能实现操作成功。但是这个场景,换幂等号风险很高,因为原本可以用订单号来做幂等号,如果换幂等号,很容易资损,调用方必须要设计一个单个订单下只有一个成功的幂等号。从整体设计来看,积分系统接口严格遵循了幂等设计,把复杂留给了调用方,但是整个流程并没有严格幂等,并引入了更复杂的处理。
综合上述分析,我们可以看到,为了应对这种场景,在设计上其实可以采取一些折衷措施。
-
结果为成功时,幂等防止重复请求
-
结果为失败时,允许重试,服务提供方重新处理流程,资源允许情况下,会将结果处理为成功。
这样的设计违背了幂等性的定义,但从实际工程的角度出发,它可以有效地解决部分场景问题。
所以 如果你是积分系统的开发者,你会设计成什么样呢?