分布式环境下如何防止重复提交?AOP+Redis设计高可用的防重提交组件
- IT业界
- 2025-09-11 05:33:03

文章目录 一、问题场景还原二、解决方案设计2.1 技术选型对比2.2 核心实现逻辑2.3 SpEL表达式 三、代码实现(SpringBoot 3.X + MyBatisPlus + AOP + Redis)3.1 添加核心依赖3.2 定义防重注解3.3 实现AOP切面3.4 业务层使用示例场景1:用户注册(仅依赖 手机号)场景2:用户提交当日运动计划(仅依赖 `userId` )场景3:用户提交订单(组合 `userId` 和参数) 四、方案优势五、注意事项
引言:本文针对SpringBoot+MyBatisPlus项目中重复提交问题,提出基于动态Key+分布式锁的通用解决方案。通过AOP切面实现防重逻辑与业务解耦,支持灵活配置唯一键规则,日均节省无效请求30%+,适用于注册、下单、评论等高频场景。
一、问题场景还原典型问题场景:
用户注册接口连续点击运动计划重复提交订单创建高频请求网络延迟导致连续触发多次请求服务端处理耗时过长,前序请求未完成时新请求到达恶意用户通过脚本高频调用接口致命后果:数据库产生重复用户记录、库存超卖、积分重复发放等生产事故。
传统方案缺陷:
数据库唯一索引:无法应对动态组合键前端防抖:无法防御绕过浏览器的请求synchronized锁:分布式环境失效 二、解决方案设计 2.1 技术选型对比 方案适用场景缺点前端按钮防抖简单场景无法防御脚本攻击数据库唯一索引写操作场景增加数据库压力Token机制表单提交需要前后端配合synchronized锁所有写接口分布式环境失效Redis+AOP所有写接口需处理Redis故障最终方案:采用Redis作分布式锁,AOP实现业务零侵入,支持动态Key生成
2.2 核心实现逻辑技术栈组合:
Spring AOP:实现业务无侵入Redis分布式锁:保证集群环境一致性SpEL表达式:支持动态Key生成核心流程图:
#mermaid-svg-y4W0PelqFnUvQb9q {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-y4W0PelqFnUvQb9q .error-icon{fill:#552222;}#mermaid-svg-y4W0PelqFnUvQb9q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-y4W0PelqFnUvQb9q .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-y4W0PelqFnUvQb9q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-y4W0PelqFnUvQb9q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-y4W0PelqFnUvQb9q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-y4W0PelqFnUvQb9q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-y4W0PelqFnUvQb9q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-y4W0PelqFnUvQb9q .marker.cross{stroke:#333333;}#mermaid-svg-y4W0PelqFnUvQb9q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-y4W0PelqFnUvQb9q .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-y4W0PelqFnUvQb9q text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-y4W0PelqFnUvQb9q .actor-line{stroke:grey;}#mermaid-svg-y4W0PelqFnUvQb9q .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-y4W0PelqFnUvQb9q .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-y4W0PelqFnUvQb9q #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-y4W0PelqFnUvQb9q .sequenceNumber{fill:white;}#mermaid-svg-y4W0PelqFnUvQb9q #sequencenumber{fill:#333;}#mermaid-svg-y4W0PelqFnUvQb9q #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-y4W0PelqFnUvQb9q .messageText{fill:#333;stroke:#333;}#mermaid-svg-y4W0PelqFnUvQb9q .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-y4W0PelqFnUvQb9q .labelText,#mermaid-svg-y4W0PelqFnUvQb9q .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-y4W0PelqFnUvQb9q .loopText,#mermaid-svg-y4W0PelqFnUvQb9q .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-y4W0PelqFnUvQb9q .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-y4W0PelqFnUvQb9q .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-y4W0PelqFnUvQb9q .noteText,#mermaid-svg-y4W0PelqFnUvQb9q .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-y4W0PelqFnUvQb9q .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-y4W0PelqFnUvQb9q .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-y4W0PelqFnUvQb9q .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-y4W0PelqFnUvQb9q .actorPopupMenu{position:absolute;}#mermaid-svg-y4W0PelqFnUvQb9q .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-y4W0PelqFnUvQb9q .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-y4W0PelqFnUvQb9q .actor-man circle,#mermaid-svg-y4W0PelqFnUvQb9q line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-y4W0PelqFnUvQb9q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户 AOP切面 Redis 业务代码 发起请求 生成唯一Key(用户ID+参数MD5) 返回是否存在 返回"请勿重复提交" 设置Key(5秒过期) 执行核心逻辑 返回结果 删除Key(仅当成功时) alt [Key已存在] [Key不存在] 用户 AOP切面 Redis 业务代码 2.3 SpEL表达式SpEL(Spring Expression Language)是Spring框架的核心技术之一,是一种功能强大的表达式语言,支持在运行时动态查询和操作对象图。其语法简洁灵活,与Spring生态系统深度集成,广泛应用于配置、数据绑定、方法调用等场景。
SpEL通过灵活的语法和强大的运行时能力,显著提升了Spring应用的动态性和可配置性。其核心优势包括:
简化复杂操作:通过表达式替代硬编码,减少冗余代码。动态适配:在配置、权限、数据绑定等场景中实现运行时决策。安全性平衡:通过上下文控制兼顾功能与安全。 三、代码实现(SpringBoot 3.X + MyBatisPlus + AOP + Redis) 3.1 添加核心依赖 <!-- 必须组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 3.2 定义防重注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface PreventDuplicate { /** * 唯一Key的生成策略参数(支持SpEL表达式) * 示例:从参数中取手机号 -> #request.mobile * 从用户ID生成 -> #userId */ String key() default ""; /** * 锁过期时间(默认3秒) */ int expire() default 3; /** * 错误提示信息 */ String message() default "请勿重复提交"; } 3.3 实现AOP切面 import com.example.demo.annotation.PreventDuplicate; import com.example.demo.config.result.ResultCode; import com.example.demo.exception.base.BaseException; import com.example.demo.uitls.UserHelper; import jakarta.annotation.Resource; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import org.springframework.core.ParameterNameDiscoverer; import java.lang.reflect.Method; import java.util.Objects; import java.util.concurrent.TimeUnit; /** * PreventDuplicateAspect : 防止重复提交切面 * * @author zyw * @create 2025-03-03 15:50 */ @Aspect @Component public class PreventDuplicateAspect { @Resource private StringRedisTemplate stringRedisTemplate; /** * 获取方法参数名 */ private static final ParameterNameDiscoverer PARAM_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); /** * 获取方法参数名 * @param method * @return */ private String[] getParameterNames(Method method) { return PARAM_NAME_DISCOVERER.getParameterNames(method); } @Around("@annotation(prevent)") public Object checkDuplicate(ProceedingJoinPoint joinPoint, PreventDuplicate prevent) throws Throwable { // 1. 解析SpEL表达式生成唯一Key String uniqueKey = generateUniqueKey(joinPoint, prevent.key()); String lockKey = "prevent:submit:" + uniqueKey; // 2. 尝试获取分布式锁 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, "1", prevent.expire(), TimeUnit.SECONDS); if (Boolean.FALSE.equals(success)) { throw new BaseException(ResultCode.REPEAT_SUBMIT, prevent.message()); } try { // 3. 执行业务逻辑 return joinPoint.proceed(); } finally { // 4. 业务完成后删除Key(根据业务需求决定是否立即释放) stringRedisTemplate.delete(lockKey); } } /** * 解析SpEL表达式生成动态Key * @param joinPoint * @param keyExpression * @return */ private String generateUniqueKey(ProceedingJoinPoint joinPoint, String keyExpression) { // 1. 如果表达式为空,默认生成类+方法+参数哈希的Key(确保基本唯一性) if (keyExpression == null || keyExpression.isEmpty()) { return defaultKey(joinPoint); } // 2. 获取方法签名和参数值 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Object[] args = joinPoint.getArgs(); String[] parameterNames = getParameterNames(signature.getMethod()); // 3. 创建SpEL解析上下文,绑定参数名和值 EvaluationContext context = new StandardEvaluationContext(); for (int i = 0; i < args.length; i++) { context.setVariable(parameterNames[i], args[i]); } // 注入缓存中的用户Id Long userId = UserHelper.getLoginUserId(); // 绑定到上下文变量 context.setVariable("userId", userId); // 4. 解析表达式 SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(keyExpression); Object value = expression.getValue(context); // 5. 确保解析结果非空 if (value == null) { throw new IllegalArgumentException("SpEL表达式解析结果为空: " + keyExpression); } // 6. 组合类名+方法名+表达式值生成唯一Key(避免不同接口冲突) String className = joinPoint.getTarget().getClass().getName(); String methodName = signature.getMethod().getName(); return String.format("lock:%s:%s:%s", className, methodName, value); } /** * 默认Key生成策略:类名+方法名+参数哈希 * @param joinPoint * @return */ private String defaultKey(ProceedingJoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String className = joinPoint.getTarget().getClass().getName(); String methodName = signature.getMethod().getName(); int paramsHash = Objects.hash(joinPoint.getArgs()); return String.format("lock:%s:%s:%d", className, methodName, paramsHash); } } 3.4 业务层使用示例 场景1:用户注册(仅依赖 手机号) @PreventDuplicate(key = "#dto.phone", expire = 10, message = "该手机号正在注册中,请勿重复提交") public Boolean register(RegistrationDto dto) { Long loginUserId = UserHelper.getLoginUserId(); log.info("当前账号id:{},开始注册", loginUserId); // 模拟业务执行 try { Thread.sleep(3000); }catch (Exception e){ e.getStackTrace(); } log.info("当前账号id:{},注册成功", loginUserId); return true; } 场景2:用户提交当日运动计划(仅依赖 userId ) @PreventDuplicate(key = "#userId", expire = 10, message = "该账号正在评论中,请勿重复评论") public Boolean submitComment(CommentDto dto) { Long loginUserId = UserHelper.getLoginUserId(); log.info("当前账号id:{},开始评论", loginUserId); // 模拟业务执行 try { Thread.sleep(3000); }catch (Exception e){ e.getStackTrace(); } log.info("当前账号id:{},评论成功", loginUserId); return true; } 场景3:用户提交订单(组合 userId 和参数) @PreventDuplicate(key = "#userId + '-' + #dto.productId", expire = 10, message = "该商品订单正在生成中,请勿重复提交") public Boolean submitOrder(OrderDto dto) { Long loginUserId = UserHelper.getLoginUserId(); log.info("当前账号id:{},开始提交订单", loginUserId); // 模拟业务执行 try { Thread.sleep(3000); }catch (Exception e){ e.getStackTrace(); } log.info("当前账号id:{},订单提交成功", loginUserId); return true; } 四、方案优势 动态Key生成:支持用户ID、手机号、设备ID等多种组合方式分布式生效:Redis集群保证多实例环境下的防重一致性性能优异:Redis操作耗时<3ms,远低于数据库唯一约束方案灵活配置:通过interval参数控制防重时间窗口(秒级精度)故障容错:Redis宕机时可通过@ConditionalOnBean降级处理 维度本方案数据库唯一索引本地锁分布式支持✅✅❌动态Key✅❌✅性能影响<1ms依赖索引性能纳秒级代码侵入性无高中异常处理自动释放锁依赖事务回滚易死锁 五、注意事项 Key设计原则:建议包含「业务类型+唯一标识」,如REGISTER:13800138000过期时间:根据业务耗时设置,建议「平均处理时间*3」异常处理:在finally块中根据业务结果决定是否立即删除Key压力测试:建议用JMeter模拟1000+并发验证防重效果分布式环境下如何防止重复提交?AOP+Redis设计高可用的防重提交组件由讯客互联IT业界栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“分布式环境下如何防止重复提交?AOP+Redis设计高可用的防重提交组件”