1、什么是幂等性?

幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

2、接口幂等有哪些使用场景?

接口幂等一般出现的场景有:

  • 前端重复提交;
  • 接口超时重试;
  • 消息队列重复消费。

3、幂等设计应该在哪一层做?

目前互联网技术架构基本都是分布式、微服务架构,层次分的也比较清晰,如:

  • 第一层:APP、H5、PC等终端访问;
  • 第二层:负载均衡设备(F5,LVS,NGINX);
  • 第三层:网关层(GateWay);
  • 第四层:业务层(Service);
  • 第五层:持久层(ORM);
  • 第六层:数据存储层(MySQL).

具体在哪一层级进行幂等设计呢?
一般网关层主要的任务是路由转发、请求鉴权和身份认证、限流、跨域、流量监控、请求日志、ACL控制等。如果在网关层实现幂等性,那需要把业务代码写在网关层,这种做法一般在设计中是很少推荐的,所以不适合。
业务层主要是处理业务逻辑,关注的重点应该是具体的业务逻辑,也不适合做幂等设计。
持久层也叫数据访问层,和数据库打交道,不做幂等性的话,写相关操作就会出现问题,所以这一层是需要做幂等性校验。

4、幂等性解决方案

幂等性解决方案可以在客户端和服务端实现,但是客户端实现,由于涉及到多设备,兼容性等问题,可靠性相对来说会差一点。

4.1 客户端幂等性控制

  • 按钮只可操作一次:一般是提交后把按钮置灰或 loding 状态,消除用户因为重复点击而产生的副作用,比如添加操作由于点击两次而产生两条记录。
  • 使用重定向机制(Post-Redirect-Get):在提交后执行页面重定向,这就是所谓的Post-Redirect-Get模式。简言之,当用户提交了表单后,去执行一个客户端的重定向,转到提交成功信息页面,这样避免用户按F5刷新导致的重复提交,而且也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样重复提交的问题。
  • token机制:token 令牌方案一般包括两个请求阶段:
  • 客户端首次请求获取token,服务端生成token返回;
  • 客户端带着token请求,服务端校验token。完整流程图如下:
  1. 客户端首次访问,发起请求,申请获取token;
  2. 服务端生成全局唯一的token,保存到redis中(一般会设置一个过期时间),然后返回给客户端;
  3. 客户端带着token,发起请求;
  4. 服务端去redis确认token是否存在,一般用 redis.del(token)的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败不处理业务逻辑,直接返回结果。

4.2 服务端幂等性控制

  • select + insert + pk/uk:根据主键或者唯一索引字段从DB查询数据,如果数据已经存在,那就是重复请求,直接返回成功;如果数据不存在,就执行 insert,如果 insert 成功,则直接返回成功,如果 insert 产生键冲突异常,则捕获异常,直接返回成功。
    完整流程图如下:

伪代码如下:

public Result idempotent(Dto dto){
    // 1、查询数据库
    Object data = getByDb(dto);
    
    // 如果数据已存在,直接返回
    if(Objects.nonNull(data)){
        log.info("重复请求,直接返回成功,参数:{}", dto);
        return Result.ok();
    }
    
    try{
        // 数据不存在,插入数据
        create(dto);
    } catch(DuplicateKeyException e){
        log.info("键冲突,是重复请求,直接返回成功,参数:{}", dto);
         return Result.ok();
    }
    
    // 正常处理请求
    doRequest();
    return Result,ok();
}
  • insert + pk/uk:此种方式是在 select + insert + pk/uk 的基础上,去掉了查询DB这一步,直接插入数据到DB,通过主键/唯一索引冲突判断是否是重复请求。
  • 状态机幂等:很多业务表,都是有状态的。比如订单表就有待支付、支付中、支付成功、支付失败、订单超时关闭等状态,在设计的时候最好只支持状态的单向改变(不可逆),这样在更新的时候 where 条件里可以加上 status = 期望的status,多次调用的话实际上也只会执行一次。
  • 接口幂等Java 接口幂等设计_数据

  • 防重表实现幂等:增加一个表,这个表叫做防重表(防止数据重复的表),使用业务表唯一主键或者唯一索引去做防重表的唯一索引,每次请求都向防重表添加一条记录,第一次请求由于没有记录,插入成功,成功后进行后续业务处理,处理完后(无论成功或失败)删除去重表中的数据,如果在处理过程中,有新的相同请求过来,插入的时候因为表中唯一索引而插入失败,则返回操作成功。
  • 乐观锁实现幂等:如果更新已有数据,可以进行加锁更新(悲观锁),也可以设计表结构时使用乐观锁,通过 version来做乐观锁,这样既能保证执行效率,又能保证幂等。乐观锁的 version 版本在更新业务数据要自增(解决ABA问题)。具体步骤如下:
  1. 先根据条件查询数据,得到对应的版本号 version;
  2. 更新数据的时候,带上版本号 version,只有版本号匹配才会更新数据,如果不匹配就不更新数据;
  3. 更新数据的时候,同时需要更新数据对应的版本号 version。
  • 分布式锁实现幂等:请求过来时,先去尝试获得分布式锁(设置一个合适的过期时间,太短,拦截不了重复请求,太长又会占用存储空间),如果获得成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。执行流程如下图所示:
  • 接口幂等Java 接口幂等设计_数据_02

  • 缓冲队列:将请求都快速地接收下来,放入缓冲队列,后续使用异步任务处理队列中的数据,过滤掉重复的情求,此方案优点是同步改为异步处理,高吞吐;不足是不能及时地返回请求结果,需要后续轮询处理结果。