如何使用自定义注解+Redis的拦截器实现幂等性校验

发布时间:2021-10-25 10:28:09 作者:iii
来源:亿速云 阅读:177
# 如何使用自定义注解+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 "请勿重复提交";
}

3.2 关键设计点

  1. SpEL表达式支持:通过key参数实现动态键生成

    @Idempotent(key = "#orderDTO.userId + '-' + #orderDTO.productId")
    
  2. 防误杀设计:默认采用类名+方法名作为前缀,避免不同方法键冲突

  3. 时效性控制:通过expiretimeUnit控制锁的有效期


四、Redis拦截器实现

4.1 核心逻辑流程图

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

4.2 Redis操作工具类

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);
    }
}

4.3 异常处理设计

自定义业务异常:

public class IdempotentException extends RuntimeException {
    private final String code;
    
    public IdempotentException(String code, String message) {
        super(message);
        this.code = code;
    }
}

五、Spring AOP整合

5.1 切面类实现

@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表达式解析实现...
    }
}

5.2 性能优化点

  1. 本地缓存:对已处理的请求增加本地缓存,减少Redis访问
  2. Lua脚本:使用Redis脚本保证SETNX+EXPIRE的原子性
  3. 热点分离:对高频业务使用独立Redis实例

六、完整代码实现

6.1 项目结构

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           ├── annotation/
│   │           │   └── Idempotent.java
│   │           ├── aspect/
│   │           │   └── IdempotentAspect.java
│   │           ├── exception/
│   │           │   └── IdempotentException.java
│   │           └── util/
│   │               └── RedisIdempotentHelper.java
│   └── resources/
│       └── application.yml

6.2 关键配置

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    timeout: 1000ms

七、测试验证方案

7.1 JUnit单元测试

@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);
        });
    }
}

7.2 压力测试

使用JMeter模拟: - 100并发重复提交 - 观察Redis内存和CPU使用率 - 验证错误率是否<0.1%


八、生产环境建议

8.1 监控指标

  1. Redis内存使用率
  2. 幂等拦截成功率
  3. 异常触发率告警

8.2 容灾方案

  1. 降级策略:Redis不可用时跳过校验
  2. 白名单:对特殊请求放行
  3. 动态调整:根据QPS自动调整过期时间

九、扩展思考

9.1 与分布式锁的区别

维度 幂等性控制 分布式锁
目的 防止重复请求 控制资源互斥访问
时效 短期(秒级) 长期(分钟级)
释放时机 自动过期 需显式释放

9.2 进阶优化方向

  1. 分段锁:对热点用户进行hash分组
  2. 异步记录:将拦截日志写入Kafka
  3. 智能过期:基于历史数据预测最优TTL

十、总结

本文实现的幂等性方案具有以下优势: - 非侵入式:通过注解实现,不影响业务代码 - 高性能:Redis内存操作响应快 - 灵活可扩展:支持SpEL动态键生成 - 生产就绪:包含异常处理和监控建议

完整代码示例已上传GitHub:项目链接(模拟)

最佳实践提示:对于金融级场景,建议结合数据库唯一索引+Redis实现双重校验 “`

这篇文章总计约6100字,包含: 1. 10个核心章节 2. 5个代码实现片段 3. 3张对比表格 4. 1个交互流程图 5. 完整的生产环境建议 6. 扩展思考方向

可根据实际需要调整各部分细节,建议配合实际代码示例演示效果更佳。

推荐阅读:
  1. 幂等性学习及接口的幂等性
  2. 怎么在Hibernate中Validation自定义注解实现校验

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

redis

上一篇:MYSQL 8在性能设计上的改变是怎么样的

下一篇:Python爬虫经常会被封的原因是什么

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》