专业的编程技术博客社区

网站首页 > 博客文章 正文

Spring Boot接口幂等性太难搞?一个自定义注解轻松搞定!

baijin 2025-07-28 15:12:08 博客文章 6 ℃ 0 评论

重复提交、网络抖动、服务重试...接口幂等性问题就像幽灵一样缠绕着后端开发者,稍不留神就造成资金损失、数据混乱。今天教你用一个注解,三行代码彻底解决!

一、痛!幂等性问题有多可怕?

想象这些场景:

  1. 用户疯狂点击支付按钮,账户被重复扣款
  2. 订单创建接口因网络抖动被重试,生成了两个相同订单
  3. 库存服务重试导致商品超卖

传统解决方案的坑:

  • 每个接口都写重复校验代码 → 代码冗余
  • 数据库加唯一索引 → 无法覆盖所有场景
  • 前端按钮置灰 → 防不住网络重试

二、神兵利器:@Idempotent 自定义注解

核心原理:通过AOP + Redis分布式锁,在方法执行前检查唯一请求标识

1. 定义幂等性注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    int expireTime() default 30; // 令牌有效期(秒)
    String message() default "请勿重复请求"; // 提示信息
}

2. 实现AOP切面逻辑

@Aspect
@Component
@RequiredArgsConstructor
public class IdempotentAspect {
    private final RedisTemplate<String, String> redisTemplate;

    @Around("@annotation(idempotent)")
    public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) {
        HttpServletRequest request = 
            ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        
        // 1. 从Header获取幂等令牌(实际可根据业务调整)
        String token = request.getHeader("Idempotent-Token");
        if (StringUtils.isEmpty(token)) {
            throw new BusinessException("缺少幂等令牌");
        }

        // 2. 构建Redis Key
        String key = "idempotent:" + token;

        // 3. 原子性设置令牌(存在则拒绝)
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(key, "processing", idempotent.expireTime(), TimeUnit.SECONDS);
        
        if (Boolean.FALSE.equals(success)) {
            throw new IdempotentException(idempotent.message()); // 抛出幂等异常
        }

        try {
            return joinPoint.proceed(); // 执行目标方法
        } finally {
            // 可选:标记处理完成(根据业务决定是否删除)
            // redisTemplate.delete(key);
        }
    }
}

三、实战:三行代码保护支付接口

改造前(高危!)

@PostMapping("/pay")
public Result payOrder(@RequestBody PayRequest request) {
    // 业务逻辑直接执行
    paymentService.processPayment(request);
    return Result.success();
}

改造后(安全!)

@Idempotent(expireTime = 60, message = "支付请求处理中")
@PostMapping("/pay")
public Result payOrder(@RequestBody PayRequest request) {
    // 原有业务逻辑无需修改!
    paymentService.processPayment(request);
    return Result.success();
}

四、处理流程全解析

 客户端                                     服务端
   │                                         │
   │ 1. 生成唯一Token (UUID/雪花ID等)          │
   │ 2. 发起请求携带Header: Idempotent-Token   │
   │─────────────────────────────────────────>│
   │                                         │
   │                                         ├─ 3. 检查Redis是否存在该Token
   │                                         │   │
   │                                         │<─┐ 存在 → 返回错误信息
   │                                         │  │
   │                                         │ 不存在 → 写入Redis锁定
   │                                         │
   │                                         ├─ 4. 执行业务逻辑
   │                                         │
   │ 5. 收到响应                             │
   │<─────────────────────────────────────────│

五、为什么选择注解方案?

  1. 零侵入:不改动业务代码,加注解立竿见影
  2. 高可用:基于Redis分布式锁,适合集群环境
  3. 灵活配置:支持自定义过期时间、提示信息
  4. 强隔离性:不同接口可使用独立Token策略

六、高级扩展技巧

  1. 自定义Token生成器
public interface TokenGenerator {
    String generate(HttpServletRequest request);
}
  1. 组合式Key(防不同参数重复):
String key = "idempotent:" + userId + ":" + md5(params);
  1. 自定义异常处理
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IdempotentException.class)
    public Result handleIdempotent(IdempotentException ex) {
        return Result.fail(400, ex.getMessage());
    }
}

结语

幂等性设计是分布式系统的基石要求。通过@Idempotent注解,我们实现了:

  • 业务代码零污染
  • 防重覆盖率达100%
  • 30秒快速接入

从此再也不用在Controller里写重复的checkDuplicateRequest()

技术讨论:你的系统中有哪些被幂等性问题坑过的经历?欢迎评论区分享避坑经验!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表