我们来一步步实现这个生产级通用的幂等与防重中间件。核心思路是利用Redis的原子性操作(如SETNX、INCR)和唯一请求令牌(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,可直接集成到生产环境!