在Spring Boot应用中,防止接口重复提交是一个常见的问题,特别是在处理表单提交或关键业务操作时。重复提交可能会导致数据重复插入、资源浪费或业务逻辑错误。以下是一些常见的方法来防止接口重复提交
前端
1.前端防抖/节流
前端可以通过JavaScript实现防抖(Debounce)或节流(Throttle)机制,控制用户在短时间内不能多次点击提交按钮。
// 简单的防抖函数示例
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
};
}
// 使用防抖函数包装提交事件
document.getElementById('submitBtn').addEventListener('click', debounce(function() {
// 提交表单的逻辑
}, 1000));
2.禁用提交按钮
这个就比较简单了,在用户点击提交按钮后,立即禁用按钮,防止用户再次点击。
<button type="submit" id="submitBtn" onclick="this.disabled=true;this.form.submit();">提交</button>
后端
1.后端幂等性校验
确保后端接口的幂等性,即同一个请求被多次执行后,产生的结果是相同的。
1.1 使用唯一请求标识(如UUID)
每次请求生成一个唯一的标识(如UUID),并在前端和后端之间传递这个标识。后端在处理请求时,首先检查这个标识是否已经处理过,若已处理则直接返回结果,不再执行后续逻辑。
@RestController
public class MyController {
private Set<String> requestIds = ConcurrentHashMap.newKeySet();
@PostMapping("/submit")
public ResponseEntity<String> submit(@RequestParam("requestId") String requestId, ...) {
if (requestIds.contains(requestId)) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("Duplicate request");
}
requestIds.add(requestId);
// 处理业务逻辑
return ResponseEntity.ok("Success");
}
}
1.2使用分布式锁
对于分布式系统,可以使用分布式锁(如Redis分布式锁)来确保同一时间只有一个请求在处理。
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@PostMapping("/submit")
public ResponseEntity<String> submit(...) {
String lockKey = "lock_key_" + someUniqueId;
Boolean success = redisTemplate.opsForValue().trySetIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
if (!success) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("Duplicate request");
}
try {
// 处理业务逻辑
return ResponseEntity.ok("Success");
} finally {
redisTemplate.delete(lockKey);
}
}
2. 乐观锁/悲观锁
悲观锁(Pessimistic Locking)
定义:悲观锁假设最坏的情况,认为每次访问数据时都可能会发生冲突,因此在整个数据处理过程中都会锁定数据,直到事务结束才会释放锁。
实现:通常通过数据库的 SELECT ... FOR UPDATE 语句来实现。
优点:
在高并发情况下,能够有效防止数据冲突。
缺点:
锁定时间长,可能导致其他事务长时间等待,影响性能。
BEGIN;
SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
-- 处理数据
UPDATE table_name SET column1 = value1 WHERE id = 1;
COMMIT;
乐观锁(Optimistic Locking)
定义:乐观锁假设最好的情况,认为每次访问数据时不会发生冲突,因此在提交更新时才会检查是否有冲突,如果有冲突则回滚事务。
实现:通常通过版本号(Version)或时间戳(Timestamp)来实现。在更新数据时,会检查当前版本号是否与上次读取时的版本号一致,如果不一致则认为数据已被其他事务修改,事务失败。
优点:
减少了锁的使用,提高了并发性能。
缺点:
在高并发情况下,可能会导致频繁的事务回滚,影响性能。
BEGIN;
SELECT version, column1 FROM table_name WHERE id = 1;
-- 假设读取到的版本号为1
-- 处理数据
UPDATE table_name SET column1 = value1, version = version + 1 WHERE id = 1 AND version = 1;
IF ROW_COUNT() = 0 THEN
ROLLBACK; -- 数据已被其他事务修改,事务失败
ELSE
COMMIT;
END IF;
可以使用mybatisplus的@Version
import com.baomidou.mybatisplus.annotation.Version;
public class YourEntity {
@Version
private Integer version;
// 其他字段...
}
3.拦截器/过滤器
我们可以自定义一个注解,在方法上使用,来配置接口是否可以重复提交。
创建自定义注解
/**
* RepeatSubmit注解用于防止接口在短时间内被重复提交
* 它通过指定的时间间隔来判断是否为重复提交,并提供了一个默认的提示消息
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
public int interval() default 5000;
/**
* 提示消息
*/
public String message() default "不允许重复提交,请稍后再试";
}
编写拦截器
/**
* 防止重复提交拦截器
*
* 该拦截器用于检查HTTP请求是否为重复提交,以防止例如表单重复提交等问题
* 它通过检查请求参数或请求头中的特定值来确定请求是否重复
* 如果检测到重复提交,将直接返回错误信息,否则允许请求继续执行
*/
@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{
/**
* 在请求处理之前进行拦截
*
* @param request 请求对象,包含请求的所有信息
* @param response 响应对象,用于向客户端发送响应
* @param handler 请求处理对象,可以判断是否为方法处理请求
* @return 返回true表示请求继续执行,返回false表示请求被拦截终止
* @throws Exception 如果处理过程中发生异常
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
// 判断handler是否为HandlerMethod类型,即是否由方法处理请求
if (handler instanceof HandlerMethod)
{
// 转换为HandlerMethod类型,获取执行的方法
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 获取方法上的RepeatSubmit注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 判断方法上是否有RepeatSubmit注解
if (annotation != null)
{
// 调用抽象方法判断是否重复提交
if (this.isRepeatSubmit(request, annotation))
{
// 如果是重复提交,返回错误信息并终止请求
AjaxResult ajaxResult = AjaxResult.error(annotation.message());
ServletUtils.renderString(response, JSON.marshal(ajaxResult));
return false;
}
}
// 不是重复提交,允许请求继续执行
return true;
}
else
{
// 如果handler不是HandlerMethod类型,直接允许请求继续执行
return true;
}
}
/**
* 验证是否重复提交由子类实现具体的防重复提交的规则
*
* @param request 请求对象,包含请求的所有信息
* @param annotation 防复注解,包含防重复提交的配置信息
* @return 返回true表示重复提交,返回false表示不是重复提交
* @throws Exception 如果验证过程中发生异常
*/
public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception;
}
// 自定义的同一个URL数据拦截器,继承自RepeatSubmitInterceptor
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{
// 用于在session中存储重复提交数据的键
public final String SESSION_REPEAT_KEY = "repeatData";
// 警惕:unchecked表示此处存在未经检查的转换
@SuppressWarnings("unchecked")
@Override
public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception
{
// 本次参数及系统时间
String nowParams = JSON.marshal(request.getParameterMap());
Map<String, Object> nowDataMap = new HashMap<String, Object>();
nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
// 请求地址(作为存放session的key值)
String url = request.getRequestURI();
HttpSession session = request.getSession();
Object sessionObj = session.getAttribute(SESSION_REPEAT_KEY);
if (sessionObj != null)
{
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url))
{
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
// 比较参数和时间来判断是否重复提交
if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval()))
{
return true;
}
}
}
// 如果不是重复提交,则将当前数据保存到session中
Map<String, Object> sessionMap = new HashMap<String, Object>();
sessionMap.put(url, nowDataMap);
session.setAttribute(SESSION_REPEAT_KEY, sessionMap);
return false;
}
/**
* 判断参数是否相同
*/
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
{
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS);
return nowParams.equals(preParams);
}
/**
* 判断两次间隔时间
*/
private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval)
{
long time1 = (Long) nowMap.get(REPEAT_TIME);
long time2 = (Long) preMap.get(REPEAT_TIME);
if ((time1 - time2) < interval)
{
return true;
}
return false;
}
}
防止接口重复提交需要结合前端和后端的多种手段来实现。前端可以通过防抖、节流和禁用按钮来减少重复提交的可能性;后端则需要通过幂等性校验、数据库约束、分布式锁和拦截器/过滤器等手段来确保接口的幂等性和数据的正确性。