通用的幂等设计

如何设计一个具有通用性的幂等处理框架

一个操作如果多次任意执行所产生的影响,均与一次执行的影响相同,我们就称其为幂等。

幂等处理在一些关键的业务场景中非常重要,比如支付场景中的支付接口需要保证幂等:

当你给微信支付发送这个付款请求后,一个顺利的场景是不会有任何错误发生的,微信支付收到你的付款请求,处理所有转账,然后返回一个 HTTP 200 消息表示交易完成。

但是,当发送的支付请求并没有返回任何结果,那该怎么办?原因可能有很多:

发出去的请求可能就压根没有到达微信就超时了;可能请求达到了微信,微信进行处理,但是在收到回复前超时了;可能请求达到了微信,微信也处理了,也发了回复,但是这个时候网络中断,导致没有收到回复,等等

比如在 MQ 消费的场景中,需要保证消息消费的幂等性,在大部分的业务场景中是不允许消息重复的,MQ 的 Exactly Once 语义也是通过 At Least Once + 幂等 的方式实现的,可见幂等处理非常关键。

我们的应用一般都是:接口请求 + 超时重试,在正常情况下一切安好,当遇到超时时,客户端会执行请求重试,然而重试的原因多种多样,也是复杂度的来源。

幂等的实现有两个关键因素:

  1. 幂等令牌(Idempotency Key)。客户端和服务器端通过什么方式来识别,这实际上是同一个请求或是同一个请求的多次尝试。这往往需要双方有一个既定的协议,比如账单号或者交易令牌,这种在同一个请求上具备唯一标识的元素,这种元素通常由客户端生成。

  2. 确保唯一性(Uniqueness Guarantee)。服务器端用什么机制去确保同一个请求一定不会被处理两次,也就是微信支付如何确保,同一笔交易不会因为客户端发送两次请求就被处理多次。

实现幂等的方式有多种,但是需要根据自己的业务场景来选择何时的幂等处理方式:

  • 利用数据库,一般做法是幂等令牌所在的列添加唯一索引,防止数据重复插入;

  • 还有一种做法是使用状态管理(成功、失败、处理中),使用乐观锁的方式更新记录(update ... set ... where state="处理中"),或者使用版本号来控制等

  • 利用 Redis,Redis 的 setnx 具备实现幂等性的能力,但是使用 Redis 实现的幂等方案具备分布式系统的固有的问题,无法保证消息处理和幂等处理的原子性,这样会产生很多bug,所以使用 Redis 来实现幂等难度是最大的,而得到的好处是Redis的性能高(这个要看场景是否真的需要那么高的性能要求?)(2021.01 补充:Redis 做幂等处理真的性能高吗?这个要看业务场景,最近遇到一个问题:项目中有一个内部服务需要消费 MQ 的消息,但是我们需要保证幂等消费,最开始的做法是 redis + 数据库唯一索引,后来经过大量测试后发现,每次判断幂等时需要访问1次redis,基本要花费几ms的时间,然而在内部服务消费 MQ 时出现重复消息的概率是非常小的,为了这么小概率的事情引入了 Redis ,多了一次 Redis 访问却牺牲了性能(增加了几ms的访问延迟,会降低消息的消费速度),而且幂等 Key 占用了了大量的 Redis 内存,交易记录的 key 是很多的;后来直接去掉 Redis 这一层,直接利用唯一索引,重复数据直接丢弃,每秒消费消息的次数得到了提升)

  • 如果场景是类似于支付接口的场景,面向外部的 API,我们需要使用 Redis 来做幂等,Redis 缓存幂等 key

  • 在防止插入重复数据时,推荐的方案是(这是一种面向外部 API 的幂等解决方案,具体还要看场景):

    1. 使用 redis 作为接口锁,防止重复请求到达数据库,对数据库性能造成冲击(初步的幂等保证及数据库保护)

    2. 使用唯一键保证不插入重复数据(最后的兜底及最终保证)

幂等实现时容易出问题的地方:

  1. 幂等令牌谁来产生、什么时候产生、怎么产生,一般幂等令牌的产生可以使用分布式 ID 生成算法

  2. 幂等令牌有没有被误删的可能性

  3. 是否存在竞态条件

  4. 对请求重试的处理,一般可以考虑采用维护处理状态:成功、失败、处理中

  5. 多级幂等保证,如果存在多个系统调用,每个调用都需要保证幂等处理

分析了什么是幂等,幂等的要素,实现幂等的几种方式以及幂等处理易出现问题的地方,那么接下来就是如何实现一个通用的幂等处理框架,来简化幂等开发。

参考

GTIS 是美团使用的一种幂等解决方案 , Talk is here. GTIS的实现思路是将每一个不同的业务操作赋予其唯一性。这个唯一性是通过对不同操作所对应的唯一的内容特性生成一个唯一的全局ID来实现的。基本原则为:相同的操作生成相同的全局ID;不同的操作生成不同的全局ID。

其他

// WARN:使用 insert ignore 如果控制不好是会丢数据的
// https://www.yiibai.com/mysql/insert-ignore.html
// https://blog.csdn.net/ZYC88888/article/details/81741135
// 唯一索引冲突会抱的错
1062 - Duplicate entry 'yiibai.com@gmail.com' for key 'email' 

最后更新于