在Web应用中,接口重复提交是一个常见问题,可能导致数据重复、业务逻辑错误等严重后果。本文将介绍如何使用Spring Boot结合Redis实现高效可靠的接口防重提交机制。
解决方案概述
核心思路
- 唯一标识生成:为每个请求生成唯一标识(用户+接口+参数)
- Redis存储:利用Redis的原子性操作存储请求标识
- 过期策略:设置合理的过期时间自动清理
- 校验机制:在业务执行前校验请求是否已处理
技术栈
- Spring Boot 2.7+
- Spring Data Redis
- Redis 5.0+
完整实现代码
1. 添加依赖 (pom.xml)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
2. 防重提交注解 (PreventDuplicateSubmit.java)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
// 锁定时间(默认5秒)
long lockTime() default 5;
// 时间单位(默认秒)
TimeUnit timeUnit() default TimeUnit.SECONDS;
// 自定义错误消息
String message() default "请勿重复提交";
// 是否包含请求参数
boolean includeParams() default true;
// 是否包含用户信息
boolean includeUser() default true;
}
3. 切面处理类 (DuplicateSubmitAspect.java)
import org.apache.commons.lang3.StringUtils;
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.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
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.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class DuplicateSubmitAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Around("@annotation(preventDuplicateSubmit)")
public Object around(ProceedingJoinPoint joinPoint, PreventDuplicateSubmit preventDuplicateSubmit)
throws Throwable {
// 获取请求信息
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 生成唯一请求标识
String requestKey = generateRequestKey(joinPoint, request, preventDuplicateSubmit);
// 尝试设置Redis锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
requestKey,
"1",
preventDuplicateSubmit.lockTime(),
preventDuplicateSubmit.timeUnit()
);
if (success == null || !success) {
// 重复提交处理
throw new DuplicateSubmitException(preventDuplicateSubmit.message());
}
try {
// 执行目标方法
return joinPoint.proceed();
} finally {
// 根据业务需求决定是否立即删除key
// 如果不删除,等待自动过期即可
// stringRedisTemplate.delete(requestKey);
}
}
private String generateRequestKey(ProceedingJoinPoint joinPoint,
HttpServletRequest request,
PreventDuplicateSubmit annotation) {
// 获取方法信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
StringBuilder keyBuilder = new StringBuilder("duplicate:submit:");
// 包含用户信息
if (annotation.includeUser()) {
String userId = getCurrentUserId(request);
keyBuilder.append("user:").append(userId).append(":");
}
// 包含方法信息
keyBuilder.append("method:").append(className).append(".").append(methodName).append(":");
// 包含请求参数
if (annotation.includeParams()) {
String params = getRequestParams(request, joinPoint.getArgs());
keyBuilder.append("params:").append(params);
}
return keyBuilder.toString();
}
private String getCurrentUserId(HttpServletRequest request) {
// 实际项目中从token或session中获取用户ID
// 这里使用简单示例
return request.getSession().getId();
}
private String getRequestParams(HttpServletRequest request, Object[] args) {
// 简单实现:组合请求参数
StringBuilder sb = new StringBuilder();
request.getParameterMap().forEach((key, values) -> {
sb.append(key).append("=").append(StringUtils.join(values, ",")).append("&");
});
// 添加方法参数
if (args != null && args.length > 0) {
for (Object arg : args) {
sb.append(arg.toString()).append(",");
}
}
return sb.toString();
}
}
4. 自定义异常类 (DuplicateSubmitException.java)
public class DuplicateSubmitException extends RuntimeException {
public DuplicateSubmitException(String message) {
super(message);
}
}
5. 全局异常处理 (GlobalExceptionHandler.java)
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DuplicateSubmitException.class)
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public ApiResponse handleDuplicateSubmit(DuplicateSubmitException e) {
return ApiResponse.error(e.getMessage());
}
}
// 简单的API响应对象
class ApiResponse {
private int code;
private String message;
private Object data;
public static ApiResponse error(String message) {
ApiResponse response = new ApiResponse();
response.code = 429;
response.message = message;
return response;
}
// getters and setters
}
6. 控制器示例 (OrderController.java)
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@PreventDuplicateSubmit(
lockTime = 10,
message = "订单提交过于频繁,请稍后再试",
includeUser = true
)
@PostMapping("/order/submit")
public ApiResponse submitOrder(@RequestBody OrderRequest orderRequest) {
// 模拟业务处理
try {
Thread.sleep(2000); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return ApiResponse.success("订单提交成功");
}
}
// 订单请求对象
class OrderRequest {
private String productId;
private int quantity;
private String address;
// getters and setters
}
// 扩展ApiResponse
class ApiResponse {
// ...
public static ApiResponse success(String message) {
ApiResponse response = new ApiResponse();
response.code = 200;
response.message = message;
return response;
}
}
技术原理详解
1. 请求标识生成策略
- 用户标识:通过session或token获取用户ID
- 方法标识:类名+方法名唯一确定接口
- 参数标识:请求参数组合的哈希值
- 组合策略:
duplicate:submit:user:123:method:com.example.OrderController.submit:params:productId=1001
2. Redis原子操作
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
key,
"1",
expireTime,
timeUnit
);
使用setIfAbsent
实现原子操作,避免并发问题:
- 检查key是否存在
- 如果不存在则设置值并过期时间
- 返回操作结果
3. 过期时间策略
- 短时效:5-30秒,适用于高频操作
- 长时效:1-5分钟,适用于关键业务
- 永久保留:不推荐,需手动清理
4. 防重提交流程
![图片[1]-Spring Boot + Redis 防止接口重复提交解决方案-QQ沐编程](https://www.qqmu.com/wp-content/uploads/2025/06/tubiao-springboot-redis.jpg)
高级优化策略
1. 分布式锁升级
// 使用Redisson实现分布式锁
@Autowired
private RedissonClient redissonClient;
public Object around(ProceedingJoinPoint joinPoint) {
RLock lock = redissonClient.getLock(requestKey);
try {
if (lock.tryLock(0, lockTime, timeUnit)) {
return joinPoint.proceed();
} else {
throw new DuplicateSubmitException("请勿重复提交");
}
} finally {
lock.unlock();
}
}
2. 参数哈希优化
private String getParamsHash(Object[] args) {
// 使用SHA-256生成参数哈希
String paramsString = Arrays.deepToString(args);
return DigestUtils.sha256Hex(paramsString);
}
3. 滑动窗口限流
// 使用Redis实现滑动窗口限流
public boolean allowRequest(String key, int maxRequests, int seconds) {
long now = System.currentTimeMillis();
long windowStart = now - seconds * 1000L;
// 删除过期记录
stringRedisTemplate.opsForZSet().removeRangeByScore(key, 0, windowStart);
// 获取当前请求数
long count = stringRedisTemplate.opsForZSet().count(key, windowStart, now);
if (count < maxRequests) {
// 添加当前请求
stringRedisTemplate.opsForZSet().add(key, String.valueOf(now), now);
return true;
}
return false;
}
4. 前端配合策略
// 前端防重提交
let submitting = false;
function submitForm() {
if (submitting) {
alert('请勿重复提交');
return;
}
submitting = true;
// 显示加载状态
showLoading();
fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(formData)
})
.then(response => {
if (response.status === 429) {
return response.json().then(data => {
alert(data.message);
});
}
return response.json();
})
.finally(() => {
submitting = false;
hideLoading();
});
}
使用场景建议
适用场景
- 订单提交、支付接口
- 表单提交(注册、反馈)
- 数据导入导出
- 关键业务状态变更
不适用场景
- 数据查询接口
- 实时性要求极高的接口
- 需要幂等性设计的接口(应使用其他方案)
性能与可靠性保障
性能优化
- Redis集群:部署Redis集群提高吞吐量
- 本地缓存:结合Caffeine实现二级缓存
- Key压缩:使用更短的key前缀
- 连接池:合理配置Redis连接池参数
可靠性保障
- 异常处理:Redis不可用时降级处理
- 监控报警:监控Redis状态和防重命中率
- 自动清理:设置合理的过期时间
- 压力测试:模拟高并发场景测试防重效果
总结
通过Spring Boot和Redis实现的接口防重提交方案具有以下优势:
- 高效性:Redis内存操作,响应时间在毫秒级
- 可靠性:原子操作保证并发安全
- 灵活性:注解配置,可定制参数
- 可扩展性:支持分布式部署
- 易集成:与Spring生态无缝集成
在实际应用中,建议结合业务场景选择合适的防重策略:
- 对普通表单使用5-10秒的短时效策略
- 对关键业务使用30-60秒的长时效策略
- 对特别重要的操作结合数据库唯一索引
这种方案能够有效防止重复提交导致的业务问题,提升系统稳定性和用户体验。
© 版权声明
本站资源来自互联网收集,仅供用于学习和交流,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。敬请谅解!
THE END