接口幂等性:基于token实现接口幂等的落地实现!

625次阅读  |  发布于10月以前

大家好,我是飘渺。

在DDD&微服务系列《并发与幂等实现方案》一文中我封装了一个公共的幂等组件,业务模块只需要加入pom依赖并在对应接口加上@Idemponent注解即可使用封装好的幂等功能。

近期,有粉丝反馈在项目中使用Token机制的幂等方案时会遇到一个问题:如果业务参数校验失败,由于幂等Key被删除,就会导致后续请求无法正常提交。今天我们来详细说明一下这个问题以及解决方案。

1. Token幂等方案回顾

在DailyMart中,基于Token的幂等方案包括以下步骤:

  1. 服务端提供获取token的接口供客户端进行调用,并将生成的token存入Redis中。
  2. 客户端请求接口时将toke和业务数据一起提交给后端服务接口。
  3. 服务端收到请求后先检查token是否存在,如果不存在则返回异常,如果存在则在处理业务后删除token。

其完整的处理流程如下图所示:

2. 存在的问题

在使用Token方案时会存在一个核心问题:是先完成业务操作后删除token,还是先删除token后执行业务操作呢?

先看第一种:

2.1 先执行业务操作再删除token

在高并发下,可能出现第一次访问时token存在,完成具体业务操作,但在还没有删除token时,客户端又携带token发起请求。此时,因为token还存在,第二次请求也会验证通过,执行具体业务操作。

对于这个问题有如下两种解决方案:

2.2 先删除token再执行业务

另一种方案是先删除token再执行业务,但这种方式也存在问题。如果业务执行超时或失败,没有向客户端返回明确结果,客户端就会进行重试,但此时之前的token已经被删除,导致被认为是重复请求,不再进行业务处理。

这种方案无需额外处理,一个token只能代表一次请求。一旦业务执行出现异常,则让客户端重新获取令牌,重新发起一次访问即可。

在DailyMart中采用的是先删除token后执行业务逻辑的方案。

3. token方案缺点

无论是先删除token还是后删除token,都会导致每次业务请求都产生一个额外的请求去获取token。然而,在生产环境中,业务失败或超时的情况并不多见,大多数请求都能成功完成。因此,为了处理这少数失败的请求,让绝大多数请求都产生额外的请求也算是一种资源的浪费。

4. 核心代码

核心代码如下

1、在aop中通过注解确认幂等方案

@Around("pointcut()")
public Object idempotentHandler(ProceedingJoinPoint joinPoint) throws Throwable {
  MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
  Method targetMethod = methodSignature.getMethod();
  Idempotent idempotent = targetMethod.getAnnotation(Idempotent.class);

  // 支持多种模式,所以使用工厂模式确定使用哪种
  IdempotentHandler instance = IdempotentHandlerFactory.getInstance(idempotent.type());
  try {
    instance.handler(idempotent, joinPoint);
    return joinPoint.proceed();
  } finally {
    // 处理后置逻辑,比如关闭分布式锁
    instance.afterProcess();
  }
}

2、在具体handler中处理幂等实现逻辑,以token机制为例只需要直接删除token。

 @Override
public void handler(Idempotent idempotent, ProceedingJoinPoint joinPoint) {
  HttpServletRequest request = ((ServletRequestAttributes)   RequestContextHolder.currentRequestAttributes()).getRequest();

  String token = request.getHeader(TOKEN_KEY);

  if (StrUtil.isBlank(token)) {
    token = request.getParameter(TOKEN_KEY);
    if (StrUtil.isBlank(token)) {
      throw new IdempotentException(IDEMPOTENT_TOKEN_ERROR);
    }
  }

  Boolean deleteFlag = distributedCache.delete(token);

  // 如果redis中已经过期,需要提示重新操作
  if (!deleteFlag) {
    throw new IdempotentException(IDEMPOTENT_TOKEN_DELETE_ERROR);
  }

}

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8