接口幂等性问题

134 阅读7分钟

前言

在日常开发接口的过程中,接口的幂等性问题是我们必须要考虑的,否则会带来很严重的后果。比如在支付场景中,用户不小心点了两次,然后就发现被扣了两次钱,这显然是很严重的问题。因此考虑接口的幂等性是很重要的。

一、什么是幂等性

维基百科解释:

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中一个幂等操作的特点是其任意多次执行所产生的影响钧与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,"setTrue()"函数就是一个幂等函数,无论多次执行,其结果都是一样的。

重点是重复执行多次的结果和执行一次的结果是一致的,因此接口的幂等性可以理解为用户在短时间内对同一接口发起了多次重复请求,和用户在短时间内发起了一次请求,对系统中数据的影响是一样的,不会因为调用多次就产生了不一样的结果。

二、什么情况下需要考虑接口幂等性问题

2.1 对数据的增加和修改操作

2.1.1 插入操作

  • 自增主键,没有幂等性,多次调用,就会产生多条数据,不具备幂等性。

  • 业务主键,具有幂等性 业务主键一般都有唯一性限制,因此重复插入也只有一条数据才能插入成功。

2.1.2 更新操作

  • 绝对更新,具有幂等性
# 这条sql不管执行多少次,对结果的影响都是一样的。
update user set username='zhangsan' where id = 1
  • 相对更新,不具有幂等性
# 这条sql每次执行,对结果的影响可能都不相同,所以不具有幂等性
update user set username='zhangsan' where id > 5

绝对更新因为条件限制只能有一条符合条件的记录被更改,而且id是具有唯一性的,因此就算执行多次,结果和执行一次也是一样的。

相对更新的条件是一个范围,如果第一次更新的时候,范围内有5条记录,下次再更新的时候范围内也有可能有6条记录,因为在第一次请求和第二次请求之间的间隔内完全有可能有其他请求执行插入操作。

三、保证幂等性的方案

对于和Web段交互的接口,我们可以在前端拦截一部分,例如防止表单重复提交,按钮置灰,隐藏,不可点击等。但是稍微懂点技术的人都知道,可以直接去调你的后台接口,所以前端的这种方式只能过滤掉一些普通用户的幂等操作。

那么后端应该怎么处理呢?主要可以通过以下几个方面进行考虑:

3.1 状态机实现幂等

状态机我的理解就是,规定一个抽象对象的状态变化顺序,每一次状态发生变化的时候,都必须符合对应的前置状态。比如一个订单的状态可以有: 待支付、支付中、支付成功、支付失败。 那我们在对一个订单进行付款的时候,状态必须是待支付才可以成功发起扣款操作,否则不进行扣款,也就是说,要想成功对一个订单进行付款,那么这个订单的状态必须是待支付状态。 这样就算一个用户在付款之后,再次点击,也不会发生重复扣款的情况,因为如果扣款成功,这个订单的状态就变成了已支付状态,那么第二次扣款时,发现状态不是待支付,则直接取消扣款。

修改订单的SQL语句如下

-- 支付订单时,加上判断条件,判断当前这个订单的状态是不是支付中的前置状态 -> 待支付
update order set status = '支付成功' where status= '待支付' and order_id='88888888'

image.png

3.2 去重表

去重表的思路很简单,每次执行完业务之前向去重表中插入一条数据,唯一业务ID可以根据参数生成,如果插入成功则执行业务逻辑,否则不执行。

表结构大致如下

CREATE TABLE `exlude_repeat` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '唯一业务ID',
   PRIMARY KEY (`id`) USING BTREE,
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='去重表';

但是这个弊端也很明显,就是对数据库的压力比较大。

3.3 Redis保证幂等

先来复习一下Redis中SETNX指令的作用

  • SETNX全称Set If Not Exists,表示只有不存在的时候才设置键值对。
  • 只有在键key不存在的情况下才能将key的值设置为value
  • 若键key已经存在则SETNX命令不做任何操作
  • SETNX设置成功则返回1表示当前进程已经获得锁
  • SETNX设置失败则返回0表示其他进程已经获得锁,当前线程不能进入临界区,当前进程可以在一个循环中不断尝试SETNX以获得锁。

也就是说,SETNX指令可以保证相同的key,只能在第一次被执行成功,后面不管请求多少次,都不会执行。 那么我们可以利用这一特性来实现接口的幂等性,整体思路就是,请求的时候需要先往redis中添加一个key,这个key可以根据业务参数来组合生成,具体用什么作为key根据实际业务场景来决定。如果执行setnx指令成功,则允许执行业务逻辑,否则就可以直接返回成功,因为如果key已经存在于redis中,说明之前已经执行成功过一次业务逻辑了,可以不用执行,这种思路也可以用来解决重复提交的问题。如果碰到执行setnx指令的异常情况,可以根据具体异常进行不同的处理。

强调一下,幂等性是不管请求了多少次,多久之后再提交,结果始终都要和提交一次之后的结果一样,所以我们为key设置过期时间其实可以去掉,但是如果没有过期时间,那么Redis占用的内存会越来越多,直到内存溢出,这显然也是一个不可忽视的问题。所以具体的处理方式还是要根据实际业务场景来进行选择。

伪代码如下:

if(redis.setnx(key,value)){
     try{
         redis.expire(key,ttl)//设置key的过期时间
         //执行业务逻辑
     }catch(Exception e){
         //业务逻辑执行失败,根据业务进行失败后处理,这里假设是删除那个key,下次再有请求过来相当于是重试了
         redis.delete(key);
     }
}else{
    //直接返回成功结果
}

代码逻辑很简单,就是先判断当前这个key是否存在,如果存在,那么说明业务逻辑已经执行过了,则可以直接返回成功的响应。

image.png

四、总结

幂等性是开发中一个很常见,也是很重要的问题,尤其是在支付这种与钱相关的业务,保证幂等性是非常重要的,实现幂等性首先要理解业务需求,根据业务需求来确定用哪种方式实现幂等性才比较合理。