背景
软件系统可能会因为网络、电脑、系统本身等原因导致的页面响应慢。这个时候为了避免用户重复点击,我们需要防止用户重复提交来避免业务/系统数据异常。
重复提交(简单)提示效果
前端控制(辅助手段)
【方法一】按钮状态控制
用户点击后立即禁用提交按钮或置灰,配合加载动画提示操作状态。该方法仅能减少误操作,无法防御恶意请求
const handleSubmit = () => {
setButtonDisabled(true); // 禁用按钮
// 发送请求
};
【方法二】变量标记法控制
var submitFlag = false;//防重复提交标志
//表单提交函数
publishA=function(){
....... // 校验逻辑(如果有)
if(!submitFlag)// 没有提交过才提交
{
submitFlag = true;
document.addform.submit();// 提交
// 其他实现可使用类似方式让提交按钮在单击后灰掉
// document.addform.disabled=true;
}
else
{
alert("请不要重复提交!");
}
}
【方法三】防抖与节流技术
通过限制高频点击(如1秒内仅允许提交一次),降低重复提交概率
特性 | 防抖 | 节流 |
---|---|---|
触发逻辑 | 延迟执行,以最后一次触发为准 | 固定间隔执行,忽略中间触发 |
适用场景 | 需等待操作停止的场景(如输入停止) | 需均匀分配触发次数的场 |
- 原生JS实现:推荐封装通用函数,支持参数化延迟时间和立即执行选项。
- Vue项目集成:使用工具库(如
js-tool-big-box
)快速调用防抖与节流方法,减少重复代码。 - React Hooks:结合
useCallback
和useRef
管理定时器状态,避免组件重复渲染
后端核心防御
【方法四】Token令牌机制
服务端生成唯一令牌(如UUID)并存储于Session/Redis,表单提交时校验令牌有效性,处理后立即销毁。可有效拦截重复请求
// Java示例:生成并校验Token
String token = UUID.randomUUID().toString();
session.setAttribute("formToken", token);
程序流程图
【方法五】session变量控制
较token令牌机制简单,在session中记录页面的提交状态
jsp页面
//设置标志变量SubmitFlag值
session.putValue("SubmitFlag","complaint_add.jsp");
control层:
public String addxxx(){
String PageFlag="";
HttpSession session;
session = getSession(true);
PageFlag=(String) session.getValue("SubmitFlag");
if (PageFlag.equalsIgnoreCase("Over")){
//repeatSubmit
return "XXX";
}
session.putValue("SubmitFlag","Over");
.......
}
或者:
在服务器端生成一个独一无二的标识符,并将其存入Session中。同时,这个标识符会被写入表单的隐藏字段内。当用户填写完信息并点击提交后,服务器会获取表单中隐藏字段的值,并与Session中的唯一标识符进行比对。如果两者相等,则正常处理该请求;如果不相等,则说明是重复提交,服务器将不再对其进行处理
【方法六】幂等性设计
通过业务唯一标识(如订单号)或数据库唯一约束,确保多次相同请求仅产生一次效果。适用于支付、订单等场景
ALTER TABLE orders ADD UNIQUE (order_no); -- 数据库唯一约束
【方法七】请求参数校验
记录请求关键参数(如用户ID+时间戳),短时间内重复参数直接拦截
数据库层加固
【方法八】唯一索引约束
对关键字段(如手机号、邮箱)添加唯一索引,从存储层拦截重复数据。需配合异常处理优化用户体验
其他补充方案
【方法九】POST-REDIRECT-GET模式
提交后重定向至结果页,避免浏览器刷新导致重复提交。
【方法十】分布式锁
在微服务架构中,通过Redis分布式锁控制并发请求,确保全局唯一性
AOP注解化集成(进阶实践)
自定义注解
定义 @NoRepeatSubmit
注解,支持配置锁超时时间和需校验的请求参数。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
int expire() default 3; // 锁超时时间(秒)
String[] params() default {}; // 参与生成Key的请求参数名
}
切面逻辑
在AOP切面中解析注解,动态生成锁Key并控制加锁/解锁流程。
@Around("@annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) {
String key = generateLockKey(joinPoint, noRepeatSubmit.params());
try {
if (!tryLock(key, noRepeatSubmit.expire())) {
return Result.error("重复提交");
}
return joinPoint.proceed();
} finally {
releaseLock(key);
}
}
场景 | 策略 |
---|---|
高并发表单提交 | 结合AOP注解 + 参数级锁粒度 + 200ms随机重试间隔。 |
短时高频操作 | 设置更短超时时间(如1秒),结合前端按钮禁用降低后端压力。 |
分布式集群环境 | 使用Redisson或RedLock算法,通过多节点多数派机制提升可靠性。 |
结语:作者建议
实际项目中大家用的前后台技术多种多样,这里作者只是举个例子,说明场景及解决方案的逻辑。在实际项目中读者根据实现情况可选用一个或多个。