我们来一步步实现这个生产级通用的幂等与防重中间件。核心思路是利用 Redis 的原子性操作(如 SETNXINCR)和 唯一请求令牌(Token) 机制。


1. 核心概念与设计图

(1) 幂等性 (Idempotency)
  • 定义: 用户对同一操作发起多次请求,其产生的效果与一次请求相同。
  • 场景: 订单支付、库存扣减、表单提交等。
  • 关键: 识别重复请求。
(2) 防重放 (Anti-Replay)
  • 定义: 防止恶意用户捕获并重复发送有效的请求。
  • 场景: 短信验证码获取、优惠券领取等。
  • 关键: 限制请求频率或次数。
(3) 设计图 (简化版)
+----------------+      1. 生成Token      +----------+
|  客户端 (前端)  | ---------------------> |  应用层  |
+----------------+                        +----------+
          |                                      |
          | 2. 携带Token请求业务                | 3. 校验Token (Redis)
          |                                      |
          v                                      v
+----------------+                        +----------+
| 业务API (后端)  | <---------------------- |  Redis  |
+----------------+   4. 业务处理 & 响应     +----------+


2. 核心实现 - 幂等性Token机制

(1) Token生成服务 (IdempotentService)
@Service
public class IdempotentService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 生成幂等性Token (存储到Redis,有效期默认30分钟)
     * @return 唯一Token字符串
     */
    public String generateToken() {
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(token, "0", 30, TimeUnit.MINUTES); // 初始状态
        return token;
    }

    /**
     * 校验Token有效性 (原子操作)
     * @param token 客户端传递的Token
     * @return true-有效且首次使用; false-无效或已使用
     */
    public boolean validateToken(String token) {
        // Lua脚本保证原子性: 检查是否存在 && 标记为已使用
        String script = "if redis.call('get', KEYS[1]) == '0' then " +
                        "redis.call('set', KEYS[1], '1') " +
                        "return true " +
                        "else return false end";
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
        return Boolean.TRUE.equals(redisTemplate.execute(redisScript, Collections.singletonList(token)));
    }
}


3. 防重放机制 - 基于时间窗口

(1) 防重服务 (AntiReplayService)
@Service
public class AntiReplayService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 检查是否允许请求 (例如:1分钟内同一用户最多3次)
     * @param key 防重Key (如: user:123:send_sms)
     * @param timeWindow 时间窗口 (秒)
     * @param maxCount 最大允许次数
     * @return true-允许; false-超过限制
     */
    public boolean allowRequest(String key, long timeWindow, int maxCount) {
        Long count = redisTemplate.opsForValue().increment(key, 1);
        if (count == 1) {
            redisTemplate.expire(key, timeWindow, TimeUnit.SECONDS); // 设置过期
        }
        return count != null && count <= maxCount;
    }
}


4. 集成Spring Boot - 自定义注解与AOP

(1) 幂等注解 (@Idempotent)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    String tokenParam() default "token"; // 客户端传递Token的参数名
}

(2) 防重注解 (@AntiReplay)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AntiReplay {
    String key() default ""; // 防重Key (支持SpEL表达式)
    long window() default 60; // 时间窗口 (秒)
    int maxCount() default 1; // 最大请求次数
}

(3) AOP切面处理 (IdempotentAspect & AntiReplayAspect)
@Aspect
@Component
public class IdempotentAspect {

    @Autowired
    private IdempotentService idempotentService;

    @Around("@annotation(idempotent)")
    public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
        // 1. 获取请求中的Token
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        String token = request.getParameter(idempotent.tokenParam());

        // 2. 校验Token
        if (!idempotentService.validateToken(token)) {
            throw new IdempotentException("重复请求或Token无效");
        }

        // 3. 执行原方法
        return joinPoint.proceed();
    }
}


5. 使用案例

(1) 订单创建 (幂等)
@RestController
@RequestMapping("/order")
public class OrderController {

    @Idempotent // 启用幂等校验
    @PostMapping("/create")
    public ResponseEntity<String> createOrder(@RequestParam String token, @RequestBody OrderDTO order) {
        // 业务逻辑...
        return ResponseEntity.ok("订单创建成功");
    }
}

(2) 短信发送 (防重)
@RestController
@RequestMapping("/sms")
public class SmsController {

    @AntiReplay(key = "'sms:' + #phone", window = 60, maxCount = 1)
    @GetMapping("/send")
    public ResponseEntity<String> sendSms(@RequestParam String phone) {
        // 发送短信逻辑...
        return ResponseEntity.ok("验证码已发送");
    }
}


6. 生产级优化

  • Redis集群: 使用Redisson实现分布式锁增强一致性。
  • Token存储: 结合JWT携带用户信息。
  • 监控告警: 记录异常日志,对接监控系统。
  • 压测: 使用JMeter模拟高并发场景。

总结

通过Redis的原子操作 + 唯一Token + AOP注解,我们实现了一个轻量级、高可用的幂等与防重中间件。代码已适配Spring Boot 3,可直接集成到生产环境!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐