最简单的6种防止数据重复提交的方法!(干货)
最简单的6种防止数据重复提交的方法!(干货)
防止数据重复提交是Web开发中常见的需求,特别是在表单提交、支付等场景中。本文将介绍在Java单机环境下实现这一功能的6种方法,从简单的前端拦截到复杂的后端处理方案,帮助开发者全面理解并解决这一问题。
模拟用户场景
假设有一个用户添加接口,需要防止用户在短时间内多次提交相同的请求。具体场景如下:
对应的Spring Boot控制器代码如下:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
public class UserController {
/**
* 被重复请求的方法
*/
@RequestMapping("/add")
public String addUser(String id) {
// 业务代码...
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
前端拦截
前端拦截是最简单直接的方式,通过在用户点击提交按钮后,将按钮设置为不可用或隐藏状态,可以有效防止正常操作下的重复提交。
执行效果如下:
但是,前端拦截存在一个致命问题:懂行的程序员或非法用户可以直接绕过前端页面,通过模拟请求来重复提交。因此,后端拦截是必不可少的。
后端拦截
后端拦截的实现思路是在方法执行之前,先判断此业务是否已经执行过,如果执行过则不再执行,否则就正常执行。将请求的业务ID存储在内存中,并通过添加互斥锁来保证多线程下的程序执行安全。
1. 基础版 - HashMap
使用HashMap存储请求ID,代码如下:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 普通 Map 版本
*/
@RequestMapping("/user")
@RestController
public class UserController3 {
// 缓存 ID 集合
private Map<String, Boolean> reqCache = new HashMap<>();
@RequestMapping("/add")
public String addUser(String id) {
synchronized (this.getClass()) {
if (reqCache.containsKey(id)) {
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
reqCache.put(id, true);
}
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
但是,HashMap会无限增长,占用越来越多的内存,因此需要优化。
2. 优化版 - 固定大小的数组
使用固定大小的数组加下标计数器的方式,实现代码如下:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
@RequestMapping("/user")
@RestController
public class UserController {
private static String[] reqCache = new String[100]; // 请求 ID 存储集合
private static Integer reqCacheCounter = 0; // 请求计数器(指示 ID 存储的位置)
@RequestMapping("/add")
public String addUser(String id) {
synchronized (this.getClass()) {
if (Arrays.asList(reqCache).contains(id)) {
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0; // 重置计数器
reqCache[reqCacheCounter] = id; // 将 ID 保存到缓存
reqCacheCounter++; // 下标往后移一位
}
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
3. 扩展版 - 双重检测锁(DCL)
使用DCL优化代码执行效率,实现代码如下:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
@RequestMapping("/user")
@RestController
public class UserController {
private static String[] reqCache = new String[100]; // 请求 ID 存储集合
private static Integer reqCacheCounter = 0; // 请求计数器(指示 ID 存储的位置)
@RequestMapping("/add")
public String addUser(String id) {
if (Arrays.asList(reqCache).contains(id)) {
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
synchronized (this.getClass()) {
if (Arrays.asList(reqCache).contains(id)) {
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0; // 重置计数器
reqCache[reqCacheCounter] = id; // 将 ID 保存到缓存
reqCacheCounter++; // 下标往后移一位
}
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
4. 完善版 - LRUMap
使用Apache Commons Collections的LRUMap,代码如下:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
import org.apache.commons.collections4.map.LRUMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
public class UserController {
private LRUMap<String, Boolean> reqCache = new LRUMap<>(100);
@RequestMapping("/add")
public String addUser(String id) {
if (reqCache.containsKey(id)) {
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
reqCache.put(id, true);
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
5. 最终版 - 封装
封装一个公共工具类,代码如下:
import org.apache.commons.collections4.map.LRUMap;
public class IdempotentUtils {
private static LRUMap<String, Boolean> reqCache = new LRUMap<>(100);
public static boolean judge(String id, Class<?> clazz) {
if (reqCache.containsKey(id)) {
return false;
}
reqCache.put(id, true);
return true;
}
}
调用代码如下:
import com.example.idempote.util.IdempotentUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
public class UserController4 {
@RequestMapping("/add")
public String addUser(String id) {
if (!IdempotentUtils.judge(id, this.getClass())) {
return "执行失败";
}
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
扩展知识 - LRUMap 实现原理分析
LRUMap
的本质是持有头结点的环回双链表结构,当使用元素时,就将该元素放在双链表header
的前一个位置,在新增元素时,如果容量满了就会移除header
的后一个元素。
总结
本文介绍了防止数据重复提交的6种方法,包括前端拦截和多种后端处理方案。这些方法各有优劣,开发者可以根据具体场景选择合适的技术方案。需要注意的是,本文的内容仅适用于单机环境下的重复数据拦截,分布式环境下的解决方案将涉及数据库或Redis等技术。