在开发中,接口防抖(防重复提交)是一个非常实用的技巧。这个功能可以有效避免由于用户误操作或网络抖动导致的重复提交问题,从而防止生成重复的业务数据。同时,这也是提升系统稳定性的重要手段之一。今天,我们就来学习如何在SpringBoot项目中实现接口防抖,并轻松规避代码崩溃的情况。
什么是接口防抖?
简单来说,接口防抖是为了防止用户短时间内重复提交相同的请求。比如,在表单提交时,用户连续多次点击“提交”按钮,或者因网络延迟导致重复请求,这些都会引发重复提交的问题。
为什么需要接口防抖?
举个例子:假设有一个“添加用户”的接口,如果没有防抖机制,用户重复点击提交按钮可能会导致数据库中插入多个相同用户记录。想象一下,一个用户点了5次,结果数据库里多了5条相同的记录……这不仅浪费资源,还可能引发业务逻辑错误。
通过接口防抖,我们可以有效避免这种情况发生,确保系统的稳定性和数据的正确性。
哪些接口需要防抖?
不是所有接口都需要防抖,只有以下几类接口比较适合:
表单提交类接口:如用户注册、订单提交等。 按钮触发类接口:如保存设置、点赞操作等。 滚动加载类接口:如下拉刷新、加载更多等。
如何判断两次请求是重复的?
要实现防抖,我们需要解决两个核心问题:
时间间隔:两次请求的时间间隔是否过短?如果时间间隔太短(比如几百毫秒内),可以认为是重复请求。 请求参数:两次请求的参数是否完全一致?如果参数相同,说明是同一个请求。
SpringBoot接口防抖实现
接下来,我们通过一个实际案例,分步骤实现接口防抖功能。
1. 定义一个防抖注解
我们通过自定义注解来实现防抖功能,只需要在需要防抖的接口上加上这个注解。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
package com.example.requestlock.lock.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 自定义注解: 用于接口防抖
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestLock {
// Redis 锁的前缀
String prefix() default "lock:";
// 锁的过期时间
long expire() default 5;
// 时间单位,默认为秒
TimeUnit timeUnit() default TimeUnit.SECONDS;
// 参数分隔符
String delimiter() default "&";
}
解释:
@Target(ElementType.METHOD)
:表示该注解只能用在方法上。prefix
:锁的前缀,防止不同业务场景的锁混淆。expire
:锁的过期时间,防止死锁。timeUnit
:锁时间的单位,默认为秒。delimiter
:参数分隔符,用于生成唯一的锁key。
2. 生成唯一Key
为了判断两次请求是否重复,我们需要生成一个唯一的Key。这个Key由请求参数和注解中的配置共同组成。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
package com.example.requestlock.lock.util;
import com.example.requestlock.lock.annotation.RequestKeyParam;
import com.example.requestlock.lock.annotation.RequestLock;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.util.ReflectionUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class RequestKeyGenerator {
/**
* 根据注解和请求参数生成唯一的锁Key
*/
public static String getLockKey(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RequestLock requestLock = method.getAnnotation(RequestLock.class);
// 拼接锁前缀
StringBuilder keyBuilder = new StringBuilder(requestLock.prefix());
// 获取方法参数
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg == null) continue;
// 获取参数中的 @RequestKeyParam 注解
Field[] fields = arg.getClass().getDeclaredFields();
for (Field field : fields) {
RequestKeyParam annotation = field.getAnnotation(RequestKeyParam.class);
if (annotation != null) {
field.setAccessible(true);
Object value = ReflectionUtils.getField(field, arg);
keyBuilder.append(requestLock.delimiter()).append(value);
}
}
}
return keyBuilder.toString();
}
}
解释:
遍历请求参数,找到加了 @RequestKeyParam
注解的字段。将这些字段的值拼接起来,生成唯一的Key。
3. 实现防抖逻辑(基于Redis)
我们使用Redis来实现防抖功能。Redis的SET NX
命令可以用来判断Key是否存在,如果不存在就设置并返回成功,否则直接返回失败。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
package com.example.requestlock.lock.aspect;
import com.example.requestlock.lock.annotation.RequestLock;
import com.example.requestlock.lock.util.RequestKeyGenerator;
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.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RedisRequestLockAspect {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 防抖切面逻辑
*/
@Around("@annotation(requestLock)")
public Object around(ProceedingJoinPoint joinPoint, RequestLock requestLock) throws Throwable {
// 生成锁Key
String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
// 尝试设置锁
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, "", requestLock.expire(), requestLock.timeUnit());
if (Boolean.FALSE.equals(success)) {
throw new RuntimeException("您的操作太快了,请稍后再试!");
}
try {
// 执行目标方法
return joinPoint.proceed();
} finally {
// 删除锁
redisTemplate.delete(lockKey);
}
}
}
解释:
setIfAbsent
:如果Key不存在,则设置并返回成功。超时机制:给锁设置一个过期时间,防止死锁。 删除锁:方法执行完成后,手动删除锁。
4. 应用防抖注解
现在,我们在需要防抖的接口上加上@RequestLock
注解即可。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/add")
@RequestLock(prefix = "user:add:", expire = 10)
public String addUser(@RequestBody @RequestKeyParam AddRequest request) {
// 模拟添加用户的操作
return "用户添加成功!";
}
}
// 请求参数对象
@Data
public class AddRequest {
@RequestKeyParam
private String userName;
@RequestKeyParam
private String userPhone;
}
解释:
@RequestLock
:开启防抖功能。@RequestKeyParam
:标记哪些字段可以用来生成唯一的Key。
温馨提示
锁过期时间的设置:过短可能导致防抖失效,过长可能引发误判,建议根据业务场景设置合理的时间。 幂等性校验:防抖只能解决短时间内的重复提交问题,无法完全保证幂等性(如数据已存在的情况)。建议结合数据库唯一约束或业务代码校验。
总结
通过今天的学习,我们掌握了在SpringBoot项目中实现接口防抖的完整流程,包括:
定义防抖注解; 生成唯一Key; 基于Redis实现防抖逻辑; 在接口中应用防抖注解。
接口防抖不仅能提高系统的稳定性,还能减少不必要的资源消耗。希望本文的内容对大家有所帮助,快去试试吧!