找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2208

积分

0

好友

314

主题
发表于 前天 01:08 | 查看: 5| 回复: 0

在分布式系统和高并发场景下,重复请求是一个常见且令人头痛的问题。无论是用户频繁点击提交按钮,网络超时导致的重试,还是消息队列的重复投递,都可能引发数据不一致或资源重复消耗的严重后果。

技术栈:Spring Boot 3.0 + JDK 17 + Spring AOP + Redis + Lua + SpEL
核心目标:构建一个开箱即用、生产就绪、注解驱动的通用幂等与防重中间件。

一、需求背景与痛点分析

1.1 常见痛点场景

  • 用户多次点击提交:例如用户快速点击“提交订单”按钮,导致后端生成多笔内容相同的订单。
  • 网络超时重试:客户端因超时重复发起请求,或第三方回调(如支付回调)因网络问题被重复触发。
  • 消息重复消费:消息队列(如Kafka, RabbitMQ)因At-least-once投递语义导致的消息重复,引发账户余额被多次扣减。
  • 业务数据重复:如考试报名场景,同一考生重复提交导致数据库出现多条相同身份证号的记录。

以上场景都违背了 幂等性(Idempotency) 的核心原则:对于同一操作(或使用相同参数的请求),无论执行一次还是多次,其结果应该是一致的。

1.2 现有解决方案的局限性

在动手造轮子之前,我们不妨先看看常见的解决方案及其缺陷。

现有防重方案与缺陷对比

  • 数据库唯一索引:简单,但仅适用于防数据重复写入的场景,对纯查询或更新操作无效,且无法应对高并发下的请求“穿透”。
  • 前端按钮防抖/禁用:体验友好,但极度不可靠,请求可被绕过(如直接调用API),无法作为核心防线。
  • Token机制:需要前后端协同配合,流程相对复杂,增加了系统设计的耦合度。
  • 手动操作Redis:虽然有效,但需要在业务代码中大量编写重复的setnxset ... NX EX逻辑,代码侵入性强,维护成本高。

正因如此,一个基于 Spring AOPRedis,以注解形式提供、对业务代码无侵入的通用防重幂等中间件显得尤为必要。这不仅是一种最佳实践,更是提升后端服务鲁棒性的重要手段。

二、整体架构与设计原理

2.1 核心处理流程

整个中间件的核心逻辑围绕一个自定义注解@Idempotent展开。当请求到达时,其处理流程清晰明了:

幂等防重中间件处理流程图

  1. 请求拦截:AOP切面拦截所有被@Idempotent注解标记的方法。
  2. 唯一键生成:解析注解中的SpEL(Spring Expression Language)表达式,结合方法参数动态生成一个全局唯一的业务键(Key)。
  3. 原子性校验:向Redis发起SET key value EX expire NX命令。这是一个原子操作,仅在Key不存在时设置成功并附加过期时间。
  4. 逻辑执行
    • 若设置成功:表明是首次请求,放行执行业务逻辑。
    • 若设置失败:表明是重复请求,根据配置的策略(如抛出异常)直接拒绝。

整个过程在毫秒级内完成,将防重压力从数据库转移到了高性能的Redis,实现了无数据库压力的并发控制。

2.2 模块化组件设计

为了实现高内聚、低耦合和良好的可扩展性,我们将系统拆分为以下核心组件,各司其职。

幂等组件职责划分

这种设计使得每个组件职责单一,例如存储层可以轻松地从Redis替换为其他分布式协调服务(如ZooKeeper),而处理策略也可以根据业务需求自定义。

三、核心代码实现解析

3.1 定义幂等注解 (@Idempotent)

注解是使用的入口,定义了幂等规则。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
    /**
     * 幂等唯一键,支持SpEL表达式,例如: 'order:' + #userId
     */
    String key();
    /**
     * 键的过期时间(秒),默认5分钟
     */
    int expire() default 300;
    /**
     * 存储的值,默认”1“
     */
    String value() default "1";
    /**
     * 重复请求处理模式
     */
    Mode mode() default Mode.REJECT;
    enum Mode {
        // 拒绝并抛出异常
        REJECT,
        // 返回缓存的结果 (需配合结果缓存使用)
        RETURN_CACHE
    }
}

key属性是整个设计的精髓,它利用 Spring SpEL 的动态能力,允许开发者灵活地组合方法参数、常量来构造唯一标识。

3.2 AOP切面逻辑 (IdempotentAspect)

切面是驱动整个流程执行的引擎。

@Aspect
public class IdempotentAspect {
    private final IdempotentService idempotentService;
    private final ExpressionParser parser = new SpelExpressionParser();
    private final StandardReflectionParameterNameDiscoverer discoverer =
            new StandardReflectionParameterNameDiscoverer();
    public IdempotentAspect(IdempotentService idempotentService) {
        this.idempotentService = idempotentService;
    }
    @Around("@annotation(idempotent)")
    public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String[] paramNames = discoverer.getParameterNames(signature.getMethod());
        Object[] args = joinPoint.getArgs();
        // 1. 解析 SpEL 表达式,生成唯一Key
        StandardEvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < args.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }
        String key = parser.parseExpression(idempotent.key()).getValue(context, String.class);
        // 2. 尝试获取幂等锁(基于Redis SET NX EX)
        if (!idempotentService.tryLock(key, idempotent.expire())) {
            // 3. 根据模式处理重复请求
            if (idempotent.mode() == Idempotent.Mode.REJECT) {
                throw new IllegalStateException("重复请求,请勿重复提交");
            }
            // TODO: Mode.RETURN_CACHE 模式(需实现结果缓存)
        }
        // 4. 执行业务方法
        return joinPoint.proceed();
    }
}

3.3 可扩展的失败处理器

为了应对不同的业务需求,我们设计了处理器接口,默认实现为空(由AOP统一抛异常),你可以自定义,例如记录日志、发送告警或返回特定格式的响应。

public interface IdempotentFailureHandler {
    void handle(String key, Method method);
}
@Component
public class DefaultIdempotentFailureHandler implements IdempotentFailureHandler {
    @Override
    public void handle(String key, Method method) {
        // 默认实现:什么也不做,交由AOP抛出异常。
        // 可在此扩展:记录监控日志、发送钉钉/企业微信告警等。
    }
}

四、实战应用案例

案例1:下单接口防重复提交

@PostMapping("/order")
@Idempotent(key = "'order:' + #userId + ':' + #goodsId", expire = 300)
public Result<String> createOrder(@RequestParam String userId, @RequestParam String goodsId) {
    // 业务下单逻辑
    orderService.create(userId, goodsId);
    return Result.success("下单成功");
}

效果:同一用户(userId)对同一商品(goodsId)在5分钟(expire=300)内,只有第一次请求会成功创建订单,后续重复请求会被立即拒绝。

案例2:考生报名防身份证号重复

@PostMapping("/enroll")
@Idempotent(key = "'enroll:' + #candidate.idCard", expire = 300)
public Result<Void> enroll(@RequestBody Candidate candidate) {
    // 防止同一身份证重复报名
    enrollmentService.save(candidate);
    return Result.OK();
}
// 考生DTO
public class Candidate {
    private String name;
    private String idCard; // 身份证号作为幂等键的一部分
    private String phone;
}

效果:生成的Key形如enroll:11010119900307XXXX。系统确保了同一身份证号在5分钟内仅能成功报名一次。

案例3:秒杀场景用户维度限流

@PostMapping("/seckill")
@Idempotent(key = "'seckill:' + #userId + ':' + #goodsId", expire = 60)
public Result<String> seckill(@RequestParam String userId, @RequestParam Long goodsId) {
    return seckillService.execute(userId, goodsId);
}

效果:在秒杀活动的高峰期,即使用户疯狂点击或前端因延迟重复发送请求,基于 Redis 的原子操作也能保证同一用户对同一秒杀商品在1分钟内仅能成功参与一次,有效防止库存超卖和请求洪峰。

五、生产环境考量:性能与可靠性

  • 高性能:核心校验基于Redis的SET NX EX原子命令,单节点QPS可达5万以上,完全满足高并发场景。
  • 强一致性:利用Redis的分布式锁语义,在集群环境下也能保证同一Key幂等性判断的一致性。
  • 安全可靠:Key通过受控的SpEL上下文生成,避免了SQL注入类的风险。同时,所有Key均设置自动过期时间,无需手动清理,无内存泄漏之忧。
  • 资源隔离:将防重压力与业务数据库解耦,利用独立的 Redis 集群承载,保障核心业务数据库的稳定。

六、如何快速集成使用?

该中间件已封装为Spring Boot Starter,只需简单两步即可引入你的项目。

  1. 引入Maven依赖
    <!-- https://mvnrepository.com/artifact/io.github.songrongzhen/once-kit-spring-boot-starter -->
    <dependency>
        <groupId>io.github.songrongzhen</groupId>
        <artifactId>once-kit-spring-boot-starter</artifactId>
        <version>1.0.0</version>
    </dependency>
  2. 在需要防重的方法上添加注解
    @Idempotent(key = "'order:' + #userId + ':' + #goodsId", expire = 300)

    配置好项目中的Redis连接后,你的接口就自动拥有了防重与幂等能力。

总结与展望

本文从实际痛点出发,深入剖析了如何利用Spring Boot 3、AOP和Redis构建一个生产级的通用幂等防重中间件。通过注解驱动和SpEL动态表达式,它实现了业务逻辑与防重逻辑的彻底解耦,兼具灵活性、高性能和易用性。

这个项目本身也是一个很好的 开源实战 案例,展示了如何设计一个可扩展的Spring Boot Starter。你可以在此基础上,继续探索和实现更多高级特性,如结合本地缓存提升性能、增加更丰富的数据统计和监控等,使其更加强大。


本文涉及的工具完整代码已开源。欢迎在 云栈社区 交流更多关于系统架构、高并发设计以及中间件开发的最佳实践。

趣味表情包




上一篇:NVIDIA推理上下文内存存储平台亮相CES 2026,基于Rubin架构重构AI记忆体
下一篇:SystemRescue 12.02:基于Arch Linux的系统救援工具包安装与使用指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-14 18:54 , Processed in 0.492304 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表