基于SpringBoot实现接口幂等性

121 阅读4分钟

什么是接口幂等性

要实现接口幂等性,首先我们需要了解什么是接口幂等性。当同一个接口被重复请求时,只允许执行一次我们称之为幂等性。那么为什么会出接口幂等性这一问题呢,可能由于网络问题而导致出现重复请求,或者由于恶意多次发送请求,导致出现接口幂等性等等其他原因。值得注意的吗,在HTTP方法中,正常情况下,GET,HEAD,PUT和DELETE 等方法都是幂等的,而POST方法是不支持幂等的,因此,大部分可能需要实现接口幂等性的都为POST。

怎么实现接口幂等性

  1. 使用唯一标识符(ID): 为每个请求分配唯一的ID,并在服务器端记录已经处理的ID。当接收到重复的请求时,服务器可以检查ID是否已经被处理,如果已经处理过则返回相同的结果,如果没有则进行处理。这种方法适用于需要在多次请求之间保持状态的情况,例如创建资源。
  2. 使用HTTP方法和URL设计: 合理设计HTTP接口的URL和使用HTTP方法可以帮助实现幂等性。GET、HEAD、PUT、DELETE等HTTP方法通常被认为是幂等的,而POST方法通常不是。使用PUT来更新资源,使用DELETE来删除资源,可以保持幂等性。
  3. 使用版本控制: 在接口中引入版本控制可以帮助实现幂等性。每个请求都带有一个版本号,服务器可以检查请求的版本号和已经处理的版本号是否一致,如果一致则返回相同的结果,如果不一致则处理请求。
  4. 使用乐观锁定: 对于需要更新资源的操作,可以使用乐观锁定来确保幂等性。客户端在每次请求中提供一个版本号或标记,服务器在更新资源时检查这个标记是否匹配当前资源的版本。如果匹配,才会执行更新操作。
  5. 使用幂等性标记: 在请求中包含一个幂等性标记,服务器在处理请求时检查这个标记,并根据标记的值来判断是否执行相同的操作。这个标记可以是一个自定义的HTTP头或请求参数。
  6. 使用事务处理: 对于需要执行多个操作的请求,可以使用事务处理来确保幂等性。事务可以保证一组操作要么全部执行成功,要么全部失败。
  7. 使用幂等性令牌: 为每个请求生成一个唯一的幂等性令牌,并在服务器端记录已经使用过的令牌。当接收到重复的请求时,服务器可以检查令牌是否已经被使用过,如果已经使用过则返回相同的结果,如果没有则进行处理。

利用SpringBoot实现接口幂等性

在本文中实现接口幂等性的操作主要基于生成唯一的token令牌并且将其令牌存入redis的方式来实现接口幂等性,并利用注解的方式实现接口幂等性操作。

  • 导入redis依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.1.3</version>
</dependency>

在application.yaml中导入:

spring:
  data:
    redis:
      database: xx
      host: xx.xx.xx
  • 实现注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
    long expireTime() default 10000;
}
  • 生成唯一token令牌
@Service
@Slf4j
public class TokenService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public boolean checkToken(String methodName, long expireTime, String reqJsonParam){
        final boolean isDuplicate;
        String uniqueParam = jdkMD5(reqJsonParam);
        String redisKey = "token:Method="+methodName+"Param="+uniqueParam;
        log.info("redisKey:{}", redisKey);
        //超时时间的时间戳
        long expireAt = System.currentTimeMillis() + expireTime;
        String val = "expireAt@" + expireAt;
        log.info("expireAt:" + expireAt);
        if (Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(redisKey, val))) {
            if (Boolean.TRUE.equals(stringRedisTemplate.expire(redisKey, expireTime, TimeUnit.MILLISECONDS))) {
                isDuplicate =  false;
            } else {
                isDuplicate =  true;
            }
        } else {
            log.info("加锁失败 failed!!key:{},value:{}",redisKey,val);
            return true;
        }
        return isDuplicate;
    }

    private static String jdkMD5(String src) {
        String res = null;
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] mdBytes = messageDigest.digest(src.getBytes());
            res = DatatypeConverter.printHexBinary(mdBytes);
        } catch (Exception e) {
            log.error("",e);
        }
        return res;
    }
}
  • 在注解的切面实现类中实现其幂等性
@Aspect
@Component
@Slf4j
public class IdempotentAspect {
    @Resource
    private TokenService tokenService;

    @Pointcut("@annotation(com.example.demo.annotation.Idempotent)")
    private void pointCut(){}

    @Around("pointCut()")
    public Object print1(ProceedingJoinPoint joinPoint) throws Throwable {
        boolean check = this.handleRequest(joinPoint);
        if(check){
            //重复请求,提示重复 报错
            log.info("重复性请求..");
            throw new Exception("请求重复");
        }
        return joinPoint.proceed();

    }

    private Boolean handleRequest(ProceedingJoinPoint joinPoint) {
        boolean result;
        log.info("========判断是否是重复请求=======");
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        //获取自定义注解值
        Idempotent idempotent = methodSignature.getMethod().getDeclaredAnnotation(Idempotent.class);
        long expireTime = idempotent.expireTime();
        // 获取参数名称
        String methodsName = methodSignature.getMethod().getName();
        String[] params = methodSignature.getParameterNames();
        //获取参数值
        Object[] args = joinPoint.getArgs();
        Map<String, Object> reqMaps = new HashMap<>();
        for(int i=0; i<params.length; i++){
            reqMaps.put(params[i], args[i]);
        }
        log.info("INFO:"+ reqMaps);
        String reqJSON = JSON.toJSONString(reqMaps);
        result = tokenService.checkToken(methodsName, expireTime, reqJSON);
        return result;
    }


}
  • 验证
@RestController
public class demoController{

    @Idempotent(expireTime = 50000)
    @PostMapping("/hello")
    public String Get(@RequestBody String a){
        System.out.println(a);
        return "hello";
    }
}