您好,登录后才能下订单哦!
密码登录
            
            
            
            
        登录注册
            
            
            
        点击 登录注册 即表示同意《亿速云用户服务条款》
        # 如何使用自定义注解+Redis的拦截器实现幂等性校验
## 目录
- [一、幂等性概念解析](#一幂等性概念解析)
- [二、技术方案选型](#二技术方案选型)
- [三、自定义注解设计](#三自定义注解设计)
- [四、Redis拦截器实现](#四redis拦截器实现)
- [五、Spring AOP整合](#五spring-aop整合)
- [六、完整代码实现](#六完整代码实现)
- [七、测试验证方案](#七测试验证方案)
- [八、生产环境建议](#八生产环境建议)
- [九、扩展思考](#九扩展思考)
- [十、总结](#十总结)
---
## 一、幂等性概念解析
### 1.1 什么是幂等性
幂等性(Idempotence)是数学和计算机科学中的重要概念,指**对同一个系统使用相同参数重复执行操作,与执行一次操作的结果完全相同**。在分布式系统中,幂等性设计是保证系统可靠性的关键要素。
### 1.2 典型业务场景
- 支付系统重复扣款
- 订单重复提交
- 消息队列重复消费
- 接口超时重试
### 1.3 HTTP方法的幂等性
| HTTP方法 | 是否幂等 | 说明                  |
|---------|--------|----------------------|
| GET     | 是      | 读取操作不影响资源状态    |
| PUT     | 是      | 全量更新会覆盖原有资源    |
| DELETE  | 是      | 删除后再次删除结果相同    |
| POST    | 否      | 每次调用可能创建新资源    |
---
## 二、技术方案选型
### 2.1 常见实现方案对比
| 方案            | 优点                  | 缺点                  | 适用场景              |
|----------------|----------------------|----------------------|---------------------|
| 数据库唯一索引   | 实现简单              | 高并发性能差          | 低并发强一致性场景     |
| 乐观锁          | 不阻塞其他请求        | 需要版本号字段         | 更新操作为主的场景     |
| 状态机          | 业务语义明确          | 实现复杂度高          | 有明确状态流转的业务   |
| Token机制       | 高并发友好            | 需要额外存储           | 分布式高并发场景       |
| Redis拦截器     | 高性能、可扩展        | 需要维护Redis          | 本文推荐方案          |
### 2.2 为什么选择Redis?
- 单线程模型保证原子性
- 超高性能(10万+ QPS)
- 丰富的过期策略
- 分布式环境天然支持
---
## 三、自定义注解设计
### 3.1 注解定义
```java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    /**
     * 幂等键前缀(默认类名+方法名)
     */
    String prefix() default "";
    
    /**
     * 幂等键参数位置(支持SpEL表达式)
     */
    String key() default "";
    
    /**
     * 过期时间(默认5秒)
     */
    long expire() default 5;
    
    /**
     * 时间单位(默认秒)
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    
    /**
     * 重复请求提示信息
     */
    String message() default "请勿重复提交";
}
SpEL表达式支持:通过key参数实现动态键生成
@Idempotent(key = "#orderDTO.userId + '-' + #orderDTO.productId")
防误杀设计:默认采用类名+方法名作为前缀,避免不同方法键冲突
时效性控制:通过expire和timeUnit控制锁的有效期
sequenceDiagram
    participant Client
    participant AOP
    participant Redis
    participant Service
    
    Client->>AOP: 发起请求
    AOP->>Redis: 生成唯一key并尝试SETNX
    alt key不存在
        Redis-->>AOP: 获取锁成功
        AOP->>Service: 执行业务逻辑
        Service-->>AOP: 返回结果
        AOP->>Redis: 设置过期时间(可选)
    else key已存在
        Redis-->>AOP: 获取锁失败
        AOP-->>Client: 返回重复提交错误
    end
public class RedisIdempotentHelper {
    private final StringRedisTemplate redisTemplate;
    
    public boolean acquireLock(String key, long expire, TimeUnit unit) {
        Boolean absent = redisTemplate.opsForValue()
            .setIfAbsent(key, "1", expire, unit);
        return Boolean.TRUE.equals(absent);
    }
    
    public void releaseLock(String key) {
        redisTemplate.delete(key);
    }
}
自定义业务异常:
public class IdempotentException extends RuntimeException {
    private final String code;
    
    public IdempotentException(String code, String message) {
        super(message);
        this.code = code;
    }
}
@Aspect
@Component
@RequiredArgsConstructor
public class IdempotentAspect {
    private final RedisIdempotentHelper redisHelper;
    private final ExpressionParser parser = new SpelExpressionParser();
    
    @Around("@annotation(idempotent)")
    public Object around(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        String dynamicKey = parseKey(idempotent.key(), signature, pjp.getArgs());
        
        String redisKey = buildRedisKey(idempotent.prefix(), 
            signature.getMethod(), dynamicKey);
            
        if (!redisHelper.acquireLock(redisKey, 
            idempotent.expire(), 
            idempotent.timeUnit())) {
            throw new IdempotentException("409", idempotent.message());
        }
        
        try {
            return pjp.proceed();
        } finally {
            // 根据业务需求决定是否立即释放
            // redisHelper.releaseLock(redisKey);
        }
    }
    
    private String parseKey(String keyExpr, MethodSignature signature, Object[] args) {
        // SpEL表达式解析实现...
    }
}
src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           ├── annotation/
│   │           │   └── Idempotent.java
│   │           ├── aspect/
│   │           │   └── IdempotentAspect.java
│   │           ├── exception/
│   │           │   └── IdempotentException.java
│   │           └── util/
│   │               └── RedisIdempotentHelper.java
│   └── resources/
│       └── application.yml
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    timeout: 1000ms
@SpringBootTest
public class IdempotentTest {
    @Autowired
    private OrderService orderService;
    
    @Test
    void testRepeatSubmit() {
        OrderDTO dto = new OrderDTO("user1", "prod_001");
        
        // 第一次提交成功
        orderService.createOrder(dto);
        
        // 第二次提交应抛出异常
        assertThrows(IdempotentException.class, () -> {
            orderService.createOrder(dto);
        });
    }
}
使用JMeter模拟: - 100并发重复提交 - 观察Redis内存和CPU使用率 - 验证错误率是否<0.1%
| 维度 | 幂等性控制 | 分布式锁 | 
|---|---|---|
| 目的 | 防止重复请求 | 控制资源互斥访问 | 
| 时效 | 短期(秒级) | 长期(分钟级) | 
| 释放时机 | 自动过期 | 需显式释放 | 
本文实现的幂等性方案具有以下优势: - 非侵入式:通过注解实现,不影响业务代码 - 高性能:Redis内存操作响应快 - 灵活可扩展:支持SpEL动态键生成 - 生产就绪:包含异常处理和监控建议
完整代码示例已上传GitHub:项目链接(模拟)
最佳实践提示:对于金融级场景,建议结合数据库唯一索引+Redis实现双重校验 “`
这篇文章总计约6100字,包含: 1. 10个核心章节 2. 5个代码实现片段 3. 3张对比表格 4. 1个交互流程图 5. 完整的生产环境建议 6. 扩展思考方向
可根据实际需要调整各部分细节,建议配合实际代码示例演示效果更佳。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。