接口幂等性设计

Posted by Jacobc' Blog on Thursday, September 24, 2020

一、什么是幂等

幂等是一个数学与计算机科学概念。

  • 在数学中,幂等用函数表达式就是:f(x) = f(f(x))。比如求绝对值的函数,就是幂等的,abs(x) = abs(abs(x))
  • 计算机科学中,幂等表示一次和多次请求某一个资源应该具有同样的副作用,或者说,多次请求所产生的影响与一次请求执行的影响效果相同。

二、为什么需要幂等

举个例子:

我们开发一个转账功能,假设我们调用下游接口超时了。一般情况下,超时可能是网络传输丢包的问题,也可能是请求时没送到,还有可能是请求到了,返回结果却丢了。这时候我们是否可以重试呢?如果重试的话,是否会多转了一笔钱呢?

当前互联网的系统几乎都是解耦隔离后,会存在各个不同系统的相互远程调用。调用远程服务会有三个状态:成功,失败,或者超时。前两者都是明确的状态,而超时则是未知状态。我们转账超时的时候,如果下游转账系统做好幂等控制,我们发起重试,那即可以保证转账正常进行,又可以保证不会多转一笔

其实除了转账这个例子,日常开发中,还有很多很多例子需要考虑幂等。比如:

  • MQ(消息中间件)消费者读取消息时,有可能会读取到重复消息。(重复消费
  • 比如提交 form 表单时,如果快速点击提交按钮,可能产生了两条一样的数据(前端重复提交

三、接口超时了,如何处理

如果调用下游接口,或者三方接口,超时了,应该如何处理?

  1. 如果提供了查询接口,就先查询,之后,成功按成功处理,失败按失败处理
  2. 如果接口有唯一性参数,则使用相同的唯一性参数重试

下面是详细两种方案处理:

  • 方案一:就是下游系统提供一个对应的查询接口。如果接口超时了,先查下对应的记录,如果查到是成功,就走成功流程,如果是失败,就按失败处理。

拿我们的转账例子来说,转账系统提供一个查询转账记录的接口,如果渠道系统调用转账系统超时时,渠道系统先去查询一下这笔记录,看下这笔转账记录成功还是失败,如果成功就走成功流程,失败再重试发起转账。

  • 方案二:下游接口支持幂等,上游系统如果调用超时,发起重试即可。

四、如何设计幂等

既然这么多场景需要考虑幂等,那我们如何设计幂等呢?

幂等意味着一条请求的唯一性。不管是你哪个方案去设计幂等,都需要一个全局唯一的ID,去标记这个请求是独一无二的。

  • 如果你是利用唯一索引控制幂等,那唯一索引是唯一的
  • 如果你是利用数据库主键控制幂等,那主键是唯一的
  • 如果你是悲观锁的方式,底层标记还是全局唯一的ID

4.1 全局的唯一性ID

全局唯一性ID,我们怎么去生成呢?你可以回想下,数据库主键Id怎么生成的呢?

是的,我们可以使用UUID,但是UUID的缺点比较明显,它字符串占用的空间比较大,生成的ID过于随机,可读性差,而且没有递增。

我们还可以使用雪花算法(Snowflake) 生成唯一性ID。

雪花算法是一种生成分布式全局唯一ID的算法,生成的ID称为Snowflake IDs。这种算法由Twitter创建,并用于推文的ID。

一个Snowflake ID有64位。

  • 第1位:一般最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
  • 接下来前41位是时间戳,表示了自选定的时期以来的毫秒数。
  • 接下来的10位代表计算机ID,防止冲突。
  • 其余12位代表每台机器上生成ID的序列号,这允许在同一毫秒内创建多个Snowflake ID。

当然,全局唯一性的ID,还可以使用百度的Uidgenerator,或者美团的Leaf

4.2 幂等设计的基本流程

幂等处理的过程,说到底其实就是过滤一下已经收到的请求,当然,请求一定要有一个全局唯一的ID标记哈。然后,怎么判断请求是否之前收到过呢?把请求储存起来,收到请求时,先查下存储记录,记录存在就返回上次的结果,不存在就处理请求。

一般的幂等处理就是这样,如下:

五、几种实现幂等方案

5.1 token 令牌

token 令牌方案一般包括两个请求阶段:

  1. 客户端请求申请获取token,服务端生成token返回
  2. 客户端带着token请求,服务端校验token

流程如下:

  1. 客户端发起请求,申请获取token。

  2. 服务端生成全局唯一的token,保存到redis中(一般会设置一个过期时间),然后返回给客户端。

  3. 客户端带着token,发起请求。

  4. 服务端去 redis 确认 token 是否存在,一般用 redis.del(token) 的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败不处理业务逻辑,直接返回结果。

5.2 状态机幂等

很多业务表是有状态的,比如转账流水表,就会有0-待处理,1-处理中、2-成功、3-失败 等状态。转账流水更新的时候,都会涉及流水状态更新,即涉及状态机 (即状态变更图)。我们可以利用状态机实现幂等,一起来看下它是怎么实现的。

假设设计一个转账后的回调接口,把处理中的转账流水更新为成功状态,SQL这么写:

update transfr_flow set status=2 where order_id='10126' and status=1;

状态机是怎么实现幂等的呢?

  • 第1次请求来时,order_id 是 10126,该流水的状态是处理中,值是 1 ,要更新为2-成功的状态 ,所以该 update 语句可以正常更新数据,sql执行结果的影响行数是1,流水状态最后变成了2。
  • 第2请求也过来了,它的订单号号还是 10126 ,因为该流水状态已经2-成功的状态 了,所以更新结果是0,不会再处理业务逻辑,接口直接返回成功。

5.3 防重表

mysql 建立一个以唯一流水号为主键或唯一索引的表,如果插入防重表冲突即直接返回成功,如果插入成功,即去处理请求。

redis 的话,根据传入参数算 hash 值,用哈希值 setnx ,如果设置失败返回 0,直接返回成功,如果设置成功返回 1,即去处理请求。

5.4 乐观锁

什么是乐观锁

乐观锁在操作数据时,认为别人不会同时在修改数据,因此乐观锁不会上锁。只是在执行更新的时候判断一下,在此期间别人是否修改了数据。

怎样实现乐观锁呢?

就是给表的加多一列version版本号,每次更新记录version都升级一下(version=version+1)。具体流程就是先查出当前的版本号version,然后去更新修改数据时,确认下是不是刚刚查出的版本号,如果是才执行更新

比如,我们更新前,先查下数据,查出的版本号是version =1

select order_id,version from order where order_id='666'

然后使用version =1 订单Id一起作为条件,再去更新

update order set version = version +1,status='P' where  order_id='666' and version =1

最后更新成功,才可以处理业务逻辑,如果更新失败,默认为重复请求,直接返回。

「如果这篇文章对你有用,请随意打赏」

Jacobc' Blog

如果这篇文章对你有用,请随意打赏

使用微信扫描二维码完成支付