面试官:写接口时有考虑过接口幂等性问题吗?

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

1. 什么是接口幂等性

维基百科解释:

  幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于`抽象代数`中。

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

通俗解释:
接口幂等性就是用户对同一接口发起了一次或多次请求之后,对数据的影响是不变的,不会因为多次请求而产生不同的结果。

2.什么情况下要考虑接口幂等性问题

  • select查询操作具有天然幂等性

    • 每次查询不会对数据产生影响。
  • insert插入操作时

    • 自增主键,没有幂等性

      • 重复插入会有多条记录被插入
    • 业务主键,具有幂等性

      • 业务主键具有唯一性,因此重复插入也只会有一条数据成功。
  • delete删除操作时

    • 绝对删除,具有幂等性

      # 不管执行多少次,都是删除这一条
      delete from user where id = 1
      
    • 相对删除,不具有幂等性

      # 这个带范围的删除,每次操作,对结果产生的影响都可能不一样,所以不具有幂等性
      delete from user where id > 5
      
  • update更新操作时,和删除操作同理

    • 绝对更新,具有幂等性

      # 这条sql不管执行多少次,对结果的影响都是一样的。
      update user set username='尚硅谷' where id = 1
      
    • 相对更新,不具有幂等性

      # 这条sql每次执行,对结果的影响可能都不相同,所以不具有幂等性
      update user set username='尚硅谷' where id > 5
      

3. 幂等的解决方案

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

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

3.1 Redis 实现幂等

用户下单支付时,不小心点了两次,这时如果没有做幂等性处理,后台就会收到多次请求,从而会产生两次扣款。


  • 用户发送请求到服务端
  • 尝试用SETNX 指令设置key 和 value,因为SETNX只能放置成功不存在的 key
    • 放置成功,设置Key的过期时间,执行业务逻辑,返回结果
    • 放置失败,返回失败结果

3.2 状态机实现幂等

比如电商的订单,订单支付状态 0-待支付 1-支付中 2-支付成功 3-支付失败

设计的时候,状态一般都是单向改变的,也就是一个订单的状态变化状态是 0 --> 1 --> 2 或者 0--> 1 --> 3

也就是说1-支付中的前置状态必须是0-待支付2-支付成功的前置状态必须是1-支付中,这样我们在更新的时候就可以加上比较条件,判断当前要更新的订单前置状态是不是我们规定的前置状态,如果不是,就不会更新了,这样多次调用也只会执行一次。

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

状态枚举

public enum OrderStatusEnum {

    UN_PAYMENT(0,0,"待支付"),
    PAYING(0,1,"支付中"),
    PAY_SUCCESS(1,2,"支付成功"),
    PAY_FAIL(0,3,"支付失败"),
    ;

    //前置状态
    private int preStatus;

    //当前状态值
    private int status;

    //状态描述
    private String desc;


    OrderStatusEnum(int preStatus, int status, String desc) {
        this.preStatus = preStatus;
        this.status = status;
        this.desc = desc;
    }
}

3.3 数据库去重表

这种方式是利用mysql唯一索引的特性。

处理流程为:

  1. 建立一张去重表,为其中一个或多个字段建立唯一索引
  2. 客户端请求服务端,服务端将请求中的一些信息插入去重表
  3. 因为表中建立了唯一索引,
    1. 如果插入成功,表示是第一次请求,则可以继续执行后面的业务逻辑。
    2. 如果插入失败,表示已经执行过当前请求,直接返回。

流程图如下

一般在实际使用过程中,插入操作需要加上事务,如果业务逻辑出现错误执行失败,防重表中数据需要回滚。

3.4 防重Token令牌

对于客户端的重复点击,或者超时重试的情况,也可以用 Token机制实现防止重复提交。简单来说,就是调用方在调用接口之前,先向后端发请求获取一个全局唯一的ID(Token),请求的时候要携带这个Token(一般是放在Headers中),后端要对这个Token进行校验,以Token作为Key,用户信息作为Value到Redis中进行键值对内容比较,如果Key存在且Value匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的Key或Value不匹配,就返回重复执行的信息,这样就可以保证幂等性操作。

整体的处理流程:

  1. 客户端先向服务端请求获取token,服务端返回token,并将token作为Key,用户信息作为Value存入Redis中(注意设置过期时间)
  2. 客户端请求业务接口,并携带token
  3. 服务端接收到请求后对token进行校验,token作为Key,在Redis中查询信息,并进行校验
    1. 如果Redis中存在Key,对比Value,如果匹配,就删除这个Key,并执行业务逻辑
    2. 如果查出来的Value值不匹配,或者不存在这个Key,则直接返回重复消费的提示信息

流程图:

注意,在并发情况下,执行 Redis 查找数据与删除需要保证原子性,否则很可能在并发下无法保证幂等性。其实现方法可以使用分布式锁或者使用 Lua 脚本来注销查询与删除操作。

4.总结

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

本文主要介绍面试时回答该问题的一些思路,后续会出用代码实现的具体解决方案。

你可能感兴趣的:(面试官:写接口时有考虑过接口幂等性问题吗?)