一种接口幂等性实现方案

接口幂等性就是用户对同一操作发起了一次或多次请求的对数据的影响是一致不变的,不会因为多次的请求而产生副作用。网上有很多资料对幂等性接口及其实现方案进行了介绍,其中应用比较广泛的是token+redis。其实这种思想的本质就是给一个请求分配一个有关联的唯一键,请求时根据这个唯一键是否存在来判断是否是重复请求。

本文也基于这种思路,通过AOP的方式设计了一种接口幂等性实现方案,通过自定义注解来控制接口幂等性,能够细化到哪些接口要满足幂等性,并且提出了同步幂等和异步幂等的概念。这种方案目前主要是解决用户在上一次请求还没结束的情况下,多次点击重复请求的问题,比如下载导出请求,这种请求一般都比较消耗资源,因此应该避免多次请求(当前可以在前端控制,但对于异步场景,这种方案也支持)。

此方案主要由两个注解和一个切面实现,具体代码如下:

//此注解用来表示哪些参数要用于幂等性校验中
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface IdempotentParam {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface IdempotentRequest {

    /**
     * 自定义子健,最终幂等性键是由用户+子健+幂等入参组成的。
     * 注意:在自定义subKey时,要考虑唯一性,因为幂等入参和用户有可能会相同,此时就需要保证subKey是唯一的。
     */
    String subKey();

    /**
     * 是否为同步幂等,同步幂等会在aop的after中自动清除缓存key重置状态,异步幂等需要在代码中手动清除缓存。
     */
    boolean syncIdempotent() default false;
}
/**
 * 幂等性接口
 * 实现逻辑:
 * 以当前用户+@IdempotentRequest.subKey+@IdempotentParam幂等入参为唯一性校验的值进行幂等性拦截,如果缓存中没有对应的键,则放入缓存并放行请求;
 * 如果缓存中有对应的键,则说明是重复请求,拦截返回;
 * 此外,如果是异步幂等,对应的业务在处理完后需要清除缓存中的键
 **/
@Aspect
@Component
@Lazy(false)
public class IdempotentAspect {
    private static Logger logger = LoggerFactory.getLogger(IdempotentAspect.class);

    /**
     * 存放每个请求线程的幂等性校验键,用于在请求结束后自动删除缓存,实现同步幂等
     */
    private static ThreadLocal<String> keyThreadLocal = new ThreadLocal<>();

    @Pointcut("@annotation(com.hxyy.exam.aop.IdempotentRequest)")
    private void cutMethod() {}

    @Around("cutMethod()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String key = this.generateKey(joinPoint);
        if (CacheUtil.hasKey(key)){
            CommonResponse<Object> response = new CommonResponse<>();
            response.setCode(ResponseEnum.DUPOLICATE_REQUEST.getEnumCode());
            response.setMsg(ResponseEnum.DUPOLICATE_REQUEST.getEnumMsg());
            return response;
        }else {
            CacheUtil.set(key,"");
            return joinPoint.proceed(joinPoint.getArgs());
        }
    }

    @After("cutMethod()")
    public void after(){
        String key = keyThreadLocal.get();
        /**
         * 如果ThreadLocal中存在key,则说明是同步幂等,因此需要在after中清除缓存
         */
        if (StringUtils.isNotEmpty(key)){
            CacheUtil.del(key);
            keyThreadLocal.remove();
        }
    }

    private String generateKey(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        String user = JwtUtil.getUserNameFromToken();
        Object[] params = joinPoint.getArgs();
        Method targetMethod = getTargetMethod(joinPoint);
        IdempotentRequest declaredAnnotation = targetMethod.getDeclaredAnnotation(IdempotentRequest.class);
        String subKey = declaredAnnotation.subKey();
        String methodName = targetMethod.getName();
        Annotation[][] parameterAnnotations = targetMethod.getParameterAnnotations();
        StringBuilder paramStr = new StringBuilder();
        for (int i = 0; i < params.length; i++) {
            for (int j = 0; j < parameterAnnotations[i].length; j++) {
                if ("IdempotentParam".equals(parameterAnnotations[i][j].annotationType().getSimpleName())){
                    paramStr.append(JSONObject.toJSONString(params[i]));
                }
            }
        }
        String key = user+subKey+ paramStr;
        /**
         * 如果是同步幂等,则使用ThreadLocal存储key,方便线程后续获取
         */
        if (declaredAnnotation.syncIdempotent()){
            keyThreadLocal.set(key);
        }
        return key;
    }

    private Method getTargetMethod(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        String methodName = joinPoint.getSignature().getName();
        Class<?> targetClass = joinPoint.getTarget().getClass();
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
        Method objMethod = targetClass.getMethod(methodName, parameterTypes);
        return objMethod;
    }
}

使用方式:

@PostMapping("/syncIdempotent")
@IdempotentRequest(subKey = "syncIdempotent",syncIdempotent = true)
public String syncIdempotent(@IdempotentParam @RequestBody Conditions conditions,@RequestParam(value = "param1") String param1, @IdempotentParam @RequestParam(value = "param") String param){
    return "同步幂等接口测试";
}
@PostMapping("/asyncIdempotent")
@IdempotentRequest(subKey = "asyncIdempotent",syncIdempotent = false)
public String asyncIdempotent(@IdempotentParam @RequestBody Conditions conditions){
    new Thread(()->{
        try{
            //业务逻辑处理
        }catch (RuntimeException e){

        }finally {
            //清除缓存
            String user = "xxx";
            String key = user+"asyncIdempotent"+ JSONObject.toJSONString(conditions);//key由用户+subKey+conditions组成
            CacheUtil.del(key);
        }
    }).run();
    return "异步幂等接口测试";
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
幂等性指的是对同一个接口多次调用,结果是一样的,不会产生副作用。在实际场景中,比如网络不稳定、请求超时等情况下,可能会导致接口被重复调用,如果接口不具备幂等性,就有可能造成数据重复提交等问题。 为了保证接口幂等性,我们可以在接口中添加某些操作,比如在数据库中添加唯一约束、使用分布式锁等等。下面是一个使用 Spring Boot 和 Redis 实现接口幂等性的示例代码: ```java @RestController public class DemoController { @Autowired private RedisTemplate<String, String> redisTemplate; @PostMapping("/demo") public String demo(@RequestParam("id") String id) { String key = "demo:" + id; ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); Boolean absent = opsForValue.setIfAbsent(key, "true"); if (absent != null && absent) { // 执行业务逻辑 // ... redisTemplate.delete(key); return "success"; } return "fail"; } } ``` 在这个示例中,我们使用 Redis 来实现接口幂等性。当第一次请求接口时,我们使用 Redis 的 setIfAbsent 方法来设置一个键值对,如果设置成功,说明这个接口还没有被调用过,可以执行业务逻辑。执行完业务逻辑之后,我们再删除这个键值对,这样下次再请求同一个接口时,就不会重复执行业务逻辑了。 需要注意的是,接口幂等性实现不是一成不变的,具体实现方式需要根据业务场景进行调整和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值