防抖:防止重复提交
在Web系统中,表单提交是一个非常常见的功能,如果不加控制,容易因为用户的误操作或网络延迟导致同一请求被发送多次,进而生成重复的数据记录。要针对用户的误操作,前端通常会实现按钮的loading状态,阻止用户进行多次点击。而对于网络波动造成的请求重发问题,仅靠前端是不行的。为此,后端也应实施相应的防抖逻辑,确保在网络波动的情况下不会接收并处理同一请求多次。
限制 用户在请求返回之前再次进行请求(前台按钮多次点击)
思路:在第一次请求时对该用户加锁,如果锁存在就不执行方法
1. 定一个注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventShaking {/*** redis锁前缀 (用于区分方法)*/String prefix() default "";/*** 用来辨别是否是一次重复的请求,支持SpEL,可以从方法的入参中获取*/String key();/*** redis锁过期时间 默认3秒*/int expire() default 3;/*** 提示信息*/String message() default "请求过于频繁,请稍后再试";}
2. AOP 切面
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Arrays;@Component
@Aspect
@Slf4j
public class PreventShakingAspect {private static final String INTERFACE_SHAKING = "interface:shaking:";@Resourceprivate RedisTemplate redisTemplate;@Pointcut("@annotation(PreventShaking)")public void pointCut() {log.info("接口防抖");}@Around("pointCut()")public Object interceptor(ProceedingJoinPoint joinPoint) {// 获取执行的方法Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;Method method = methodSignature.getMethod();PreventShaking shaking = method.getAnnotation(PreventShaking.class);// 创建 SpEL 上下文StandardEvaluationContext context = new StandardEvaluationContext();// 设置方法参数作为变量Object[] args = joinPoint.getArgs();String[] parameterNames = ((MethodSignature) signature).getParameterNames();for (int i = 0; i < args.length; i++) {// 将参数放入上下文context.setVariable(parameterNames[i], args[i]);}// 解析 EL 表达式ExpressionParser parser = new SpelExpressionParser();Expression expression = parser.parseExpression(shaking.key());String key = StringUtils.hasText(shaking.prefix()) ? shaking.prefix() : method.getName();key = INTERFACE_SHAKING + key + ":" + expression.getValue(context);// 如果key存在缓存中 返回0;不存在就添加并访问1String script = "if redis.call('exists', KEYS[1]) == 1 then\n" +" \n" +" return 0\n" +"else\n" +" redis.call('set', KEYS[1], \"1\", \"ex\", ARGV[1])\n" +" \n" +" return 1\n" +"end";DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);Object execute = redisTemplate.execute(redisScript, Arrays.asList(key), shaking.expire());if (Long.valueOf(String.valueOf(execute)) <= 0){log.warn("您的操作太快了,请稍后重试");throw new RuntimeException(shaking.message());}try {return joinPoint.proceed();} catch (Throwable e) {log.error("系统异常", e);throw new RuntimeException("系统异常");}finally {// 方法执行结束,释放锁redisTemplate.delete(key);}}}
参考:
SpringBoot 接口防抖(防重复提交)的一些实现方案_requestlock-CSDN博客
一个注解,优雅的实现接口幂等性!-CSDN博客
java接口防抖的优雅处理_java 接口防抖-CSDN博客