如何实现接口幂等性?

1,924 阅读9分钟

什么是幂等性?

幂等本来是一个数学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。即

如果一个函数 f(x) 满足:f(f(x)) = f(x),则函数 f(x) 满足幂等性。

这个概念被拓展到计算机领域,被用来描述一个操作、方法或者服务。在计算机领域中,一个幂等操作的特点是,其任意多次执行所产生的影响均与一次执行的影响相同。

幂等性的应用场景

  • HTTP 接口设计成幂等的,可以解决客户端重复提交表单数据的问题。

  • MQ消费者方法设计成幂等的,可以解决消息重复的问题。

  • 微服务接口设计成幂等的,解决 RPC 框架超时自动重试导致的重复调用问题。

下面将会以HTTP接口幂等性来介绍幂等性的实现方案,但这些方案同样适用于其他场景。

引入幂等性的影响?

幂等性是为了简化客户端逻辑处理,能防止重复提交等操作,但却增加了服务端的逻辑复杂性和成本,其主要是:

  • 把并行执行的功能改为串行执行,降低了执行效率。

  • 增加了额外控制幂等的业务逻辑,复杂化了业务功能;

所以在使用时候需要考虑是否引入幂等性的必要性,根据实际业务场景具体分析。

Restful API 接口的幂等性设计

现在流行的 Restful 推荐的几种 HTTP 接口方法中,其幂等性可以分为下面几类:

  • √ 满足幂等
  • x 不满足幂等
  • -可能满足也可能不满足幂等,根据实际业务逻辑有关
方法类型是否幂等描述
GetGet方法一般用于获取资源。其一般不会也不应当对系统资源进行改变,所以是天然幂等的。
PostxPost方法一般用于创建新的资源。其每次执行都会新增数据,所以不是幂等的。
Put-Put方法一般用于修改资源。该操作分情况来判断是不是满足幂等,更新操作中直接根据某个值进行更新,也能保证幂等。不过执行累加操作的更新是非幂等的。
Delete-Delete方法一般用于删除资源。该操作分情况来判断是不是满足幂等,当根据为唯一值进行删除时,删除同一个数据多次执行效果一样。不过需要注意,带查询条件的删除就不一定满足幂等了。例如在根据条件删除一批数据后,这时候又新增了一条满足条件的数据,然后又一次执行了删除,那么会导致这条满足条件的数据也会被删除。

接口幂等实现原理

实现幂等性的最好方式,就是从业务逻辑设计上入手,将消费的业务逻辑设计成具备幂等性的操作。但是,不是所有的业务都能设计成天然幂等的,那就需要一些方法和技巧来帮助我们实现幂等了。

要实现接口的幂等性,实际上就是要在接口中加上接口是否重复执行的额外判断。而能够要判断接口是否重复执行,有一个隐含的前提条件就是,接口需要有一个唯一标志来表示哪些调用代表的是同一个请求,并且每次请求都需要在请求中携带该唯一标志。该唯一标志的来源通常可以是下面一些情况:

  • 业务接口自身的某些业务数据作为唯一标志。就具有唯一性,比如说,唯一的订单号就可以作为唯一标志(也可以是多个业务字段的组合)。

  • 通过外部生成非业务属性的唯一标志。比如说,服务端提供一个唯一标志的生成接口,客户端每次调用业务接口前,都去调用生成唯一标志的接口来获取唯一标志,并在请求业务接口时,都携带上该唯一标志(可以放在请求头中)

接口幂等性的判断流程一般如下:

  1. 调用接口时,通过某种方式来判断接口是否重复执行(不管什么方式,都必须借助唯一标志来判断)。
  2. 若为第一次执行,则需标志接口为已执行,然后继续执行实际业务。
  3. 若为重复执行,则返回重复执行的响应。

注意:

  • 判断接口是否重复执行的操作标记接口为已执行的操作需要保证原子性,否则无法保证并发下的幂等性。
  • 实际业务如果执行失败,则需要重新标志接口为未执行过。

通过不同方式来判断接口是否重复执行,都必须借助到唯一标志来判断,不同的判断方式其实可以分为下面两类:

方式一:查询+保存(查询唯一标志是否存在,不存在则保存)

服务端无需在接口调用前保存唯一标志,判断唯一标志不存在表示第一次执行 具体步骤如下:

  1. 调用接口时,服务端判断唯一标志是否存在
  2. 若唯一标志不存在,则表示接口第一次执行,然后需保存该唯一标志(用于标记接口为已执行),再继续执行实际业务
  3. 若唯一标志存在,则表示接口重复执行,就直接返回重复执行的响应。

方式二:查询+删除(查询唯一标志是否存在,存在则删除)

服务端需要在接口调用前保存唯一标志,判断唯一标志存在表示第一次执行。 具体步骤如下:

  1. 调用接口前,服务端需要保存唯一标志
  2. 调用接口时,服务端判断唯一标志是否存在
  3. 若唯一标志存在,则表示接口第一次执行,然后需删除该唯一标志(用于标记接口为已执行),再继续执行实际业务
  4. 若唯一标志不存在,则表示接口重复执行,就直接返回重复执行的响应。

接口幂等实现方案

通过不同方式来判断接口是否执行过,就产生了不同的接口幂等性的实现方案,下面我们介绍一些常用的实现方案。

数据库唯一索引(查询+保存)

数据库的唯一索引保证了数据插入的唯一性。我们可以通过唯一索引字段的插入来判断接口是否重复执行,流程如下:

  1. 将接口的唯一标志字段作为表的唯一索引

  2. 接口被调用时,通过插入该唯一字段来判断接口是否重复执行。

  3. 如果插入操作成功,说明唯一标志字段未被插入过,则表示接口是第一次执行,且唯一标志的插入保存就标记了接口为已执行(数据库的插入操作保证了查询+保存的原子性)

  4. 如果插入操作失败,说明唯一标志字段已经被插入过,则表示接口是重复执行。

当接口自身业务就为插入数据的业务时,该方案尤为适用。

数据库乐观锁(查询+删除)

我们知道,数据库可以在一条语句中同时查询并更新该字段,并且可以保证查询和更新两个操作是原子性的,我们可以通过该方式来实现接口的幂等性,我们将其称作数据库乐观锁方案。执行流程如下:

  1. 在表中添加版本字段(也可以是本身的业务字段,但版本字段更通用),作为接口的唯一标志
  2. 接口被调用时,通过一条SQL来查询并更新该版本字段来判断该接口是否重复执行,比如下面的示例:
UPDATE my_table SET price=price+50,version=version+1 WHERE id=1 AND version=5
  1. 如果更新操作成功,说明版本字段未被更新过,则表示接口是第一次执行,且版本字段自动加1就标记了接口为已执行(就相当于删除了之前的唯一标记,而数据库更新操作保证了查询+删除的原子性)
  2. 如果更新操作失败,说明版本字段已经更新过了,则表示接口是重复执行。

当接口自身业务就为更新数据的业务时,该方案尤为适用。

Redis SETNX(查询+保存)

Redis的SETNX操作可以判断在key不存在时,设置对应的value,且保证判断和设值两个操作的原子性。我们可以通过SETNX操作来实现接口的幂等性。执行流程如下:

  1. 接口被调用时,将唯一标志作为key,通过执行SETNX操作来判断接口是否重复执行。(SETNX操作必须要设置过期时间)

  2. 如果操作执行成功,说明key值未被设置过,则表示接口是第一次执行,且设置的key值就标记了接口为已执行(SETNX保证了查询+保存的原子性)

  3. 如果操作执行失败,说明key值已经设置过,则表示接口是重复执行。

注意:上面步骤中 SETNX 操作一定要设置过期时间。如果不设置过期时间,就会导致大量数据永久存储在 Redis中而导致异常。但是这样只能保证在过期时间内的接口幂等性。

Redis Token机制(查询+删除)

token就代表前面提到的唯一标志。Token机制实现幂等性的主要流程如下:

  1. 服务端需要提供一个接口返回token。对于需要保证幂等性的接口,客户端在执行业务接口前,都需要先调用获取token的接口,服务器也会把token保存在redis中。

  2. 客户端在调用业务接口时,需要携带token(一般放在请求头部)。

  3. 接口被调用时,服务端通过查询token是否存在redis中来判断接口是否重复执行。

  4. 若token存在redis中,则表示接口是第一次执行,然后把redis中的token删除,用于标志该接口为已执行。

  5. 若token不存在redis中,则表示接口为重复执行。

注意:执行查询判断token是否存在与删除token需要保证原子性

参考文章: