高并发必备:自定义注解 + 切面 + Redis,优雅解决重复提交问题

张开发
2026/6/11 23:40:39 15 分钟阅读
高并发必备:自定义注解 + 切面 + Redis,优雅解决重复提交问题
在 Web 项目开发中重复提交是一个非常常见的问题。用户手抖 网络卡顿用户疯狂点击“提交”按钮。多窗口操作 用户打开多个标签页同时操作同一个业务。恶意刷单 接口被恶意重复调用。如果不进行处理会导致数据库产生脏数据如重复订单甚至造成严重的业务损失。传统的做法是在每个 Controller 方法里写 Redis 逻辑代码冗余且难以维护。本文将介绍如何利用 Spring AOP面向切面编程 和 Redis 分布式锁通过自定义注解的方式优雅地解决这一问题。自定义注解 (NoRepeatSubmit) 作为一个标记告诉系统哪些接口需要防重。AOP 切面拦截 拦截所有带有该注解的方法。Redis 分布式锁 利用 Redis 的 setIfAbsent (SETNX) 原子操作。Key 的生成策略 用户ID 请求URL确保同一用户在同一接口的并发请求互斥。加锁 请求进来先尝试加锁设置过期时间防止死锁。执行业务 加锁成功执行目标方法。释放锁 方法执行完毕或异常删除 Key。代码实现1. 自定义注解Target({ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Documented public interface NoRepeatSubmit { /** * 锁的过期时间(秒) */ int expireTime() default 5; }2.编写 AOP 切面组件package com.a.background.common.aop; import com.a.background.common.anno.NoRepeatSubmit; import com.a.background.shiro.AccountProfile; import com.a.background.util.ShiroUtil; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.concurrent.TimeUnit; Aspect Component public class RepeatSubmitAspect { Autowired private RedisTemplateString, Object redisTemplate; /** * 环绕通知拦截带有 NoRepeatSubmit 注解的方法 */ Around(annotation(noRepeatSubmit)) public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) throws Throwable { // 1. 获取当前请求信息 ServletRequestAttributes attrs (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attrs null) { return joinPoint.proceed(); } HttpServletRequest request attrs.getRequest(); // 2. 构建唯一的锁 Key // 策略用户ID URL保证同一用户在同一接口的并发互斥 AccountProfile profile ShiroUtil.getProfile(); String userId (profile ! null) ? String.valueOf(profile.getId()) : anonymous; String url request.getRequestURI(); // 这里的 Key 格式可以根据业务调整例如加入参数 MD5 值做更细粒度控制 String lockKey lock:repeat: userId : url; // 3. 尝试获取分布式锁 (SETNX) // 注意必须同时设置过期时间防止业务异常导致死锁 Boolean isLocked redisTemplate.opsForValue().setIfAbsent( lockKey, 1, noRepeatSubmit.expireTime(), TimeUnit.SECONDS ); if (Boolean.TRUE.equals(isLocked)) { try { // 4. 锁获取成功执行目标方法 return joinPoint.proceed(); } finally { // 5. 方法执行结束释放锁 redisTemplate.delete(lockKey); } } else { // 6. 锁获取失败说明有重复请求正在处理 throw new RuntimeException(请勿重复提交请稍后再试); } } }3. 业务使用PostMapping(/createOrder) NoRepeatSubmit(expireTime 10) // 10秒内不允许重复提交 public Result createOrder(RequestBody OrderDTO orderDTO) { // 业务逻辑... return Result.success(); } 方案优缺点分析优点非侵入性业务代码干净整洁通过注解解耦。通用性强适用于所有需要防重的接口。安全性高利用 Redis 原子操作在分布式环境下依然有效。缺点与优化方向误伤问题如果用户真的需要在短时间内提交两次不同的数据例如快速录入两条不同的表单当前的URL作为 Key 会拦截。优化方案可以将请求参数JSON Body进行 MD5 加密拼接到 Key 中实现“相同参数防重不同参数放行”。锁释放当前代码在finally块中直接删除 Key。在极端情况下如业务执行时间超过锁过期时间可能会误删其他线程的锁。优化方案使用 Redisson 框架或 Lua 脚本实现更严谨的锁释放逻辑校验 Value 是否属于当前线程。通过 AOP Redis 实现防重提交是后端开发中提升系统健壮性的常用手段。它不仅能防止用户误操作还能在一定程度上防御恶意攻击。

更多文章