一种接口幂等性实现方案

本文介绍了如何通过AOP和自定义注解实现接口幂等性,防止因用户重复请求导致的资源浪费。方案包括同步幂等和异步幂等,通过在请求中设置唯一键并利用缓存进行校验,同步幂等在请求结束后自动清除缓存,异步幂等则需在业务处理完成后手动清除。示例代码展示了注解的使用方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

接口幂等性就是用户对同一操作发起了一次或多次请求的对数据的影响是一致不变的,不会因为多次的请求而产生副作用。网上有很多资料对幂等性接口及其实现方案进行了介绍,其中应用比较广泛的是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 "异步幂等接口测试";
}
### Java 中实现接口幂等性的方法 为了确保接口幂等性,在 Java 应用程序中通常会采用多种策略和技术来防止重复提交请求或执行操作。下面介绍几种常见的技术手段并提供相应的代码示例。 #### 1. 使用唯一标识符验证幂等性 当客户端发起请求时,服务器端可以通过校验唯一标识符(如 UUID 或订单号)来判断该请求是否已经被处理过。如果已经存在,则不再重新处理而是返回之前的结果。 ```java public class IdempotentService { private final Map<String, String> processedRequests = new ConcurrentHashMap<>(); public synchronized Response handleRequest(String uniqueId, Request request) { if (processedRequests.containsKey(uniqueId)) { return new Response(processedRequests.get(uniqueId)); } else { // 处理业务逻辑... String result = processBusinessLogic(request); // 将结果缓存起来以便后续查询 processedRequests.putIfAbsent(uniqueId, result); return new Response(result); } } } ``` 此段代码展示了如何利用 `ConcurrentHashMap` 来存储已处理过的请求及其对应的响应数据[^3]。 #### 2. 利用数据库事务与乐观锁控制并发访问 对于涉及数据库更新的操作来说,可以借助于版本字段配合 SQL 的 `UPDATE ... WHERE version = ?` 语句以及设置合适的隔离级别来达到防重的效果。这种方式被称为“乐观锁定”。 ```sql -- 假设有一个表 t_order(id INT PRIMARY KEY, status VARCHAR(20), version INT) UPDATE t_order SET amount=amount+?,version=version+1 WHERE id=? AND version=? ``` 在应用程序层面: ```java @Transactional(isolation = Isolation.READ_COMMITTED) public void updateOrderAmount(Long orderId, BigDecimal increment) throws Exception{ Order order = orderRepository.findById(orderId).orElseThrow(() -> new RuntimeException("Order not found")); int rowsAffected = jdbcTemplate.update( "UPDATE t_order SET amount=?,version=version+1 WHERE id=? AND version=?", order.getAmount().add(increment), orderId, order.getVersion() ); if(rowsAffected == 0){ throw new OptimisticLockingFailureException(); } } ``` 上述例子中,一旦检测到记录被其他事务修改则抛出异常终止当前流程,从而避免了脏读等问题的发生[^4]。 #### 3. 分布式环境下的全局唯一 ID 和分布式锁 在一个分布式的微服务体系结构里,可能还会遇到跨多个实例间的竞争条件。此时除了要保证本地服务内的等外还需要考虑整个集群范围的一致性问题。一种解决方案就是引入像 Redis 这样的中间件作为协调者生成全局唯一的序列号或者是基于它构建简单的分布式互斥锁机制。 ```java // 获取Redis连接对象 Jedis jedis = pool.getResource(); try { // 设置key的有效期为5秒,并尝试加锁 Long lockResult = jedis.setnx(lockKey, Thread.currentThread().getName()); Boolean expireSuccess = jedis.expire(lockKey, 5); if ((lockResult != null && lockResult.equals(1L))) { try { // 执行具体业务逻辑... } finally { // 解除锁定 jedis.del(lockKey); } } else { log.warn("{} failed to acquire the distributed lock",Thread.currentThread().getName()); } } catch(Exception e){ ... }finally{ jedis.close(); } ``` 这段代码片段实现了通过 Redis 实现简单版的分布式锁功能,有效解决了高并发场景下资源争抢的问题[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值