重复提交怎么拦截?8种实战方案详解(附代码与避坑指南)
📖 目录导读
- 为什么必须拦截重复提交? – 真实案例与损失分析
- 前端拦截方案 – 按钮防抖、置灰、Token机制
- 后端拦截方案 – 幂等表、Redis锁、数据库唯一索引
- 分布式场景下的终极方案 – 基于Redisson的分布式锁
- 高并发下的陷阱 – 锁过期、ABA问题、降级策略
- Q&A常见问题 – 用户反复刷新/断网重连怎么办?
- 实战代码一键复用 – 拦截器+注解+全局配置
为什么必须拦截重复提交?
真实场景还原
某电商平台用户支付时因网络延迟连点3次“立即付款”,结果:

- 系统创建了3笔相同订单
- 库存扣减3次,导致超卖
- 用户收到3条扣费短信,引发客诉
核心风险:
- 数据不一致(订单、库存、余额)
- 系统资源浪费(重复调用外部接口)
- 业务逻辑错乱(如优惠券被重复使用)
行业共识:
重复提交是系统“慢性毒药”,99%的线上Bug源于未做拦截。
前端拦截方案(第一道防线)
1 按钮防抖(Debounce)
// Vue3 组合式API
const submit = debounce(async () => {
await doSubmit()
}, 300) // 300ms内重复点击只执行最后一次
2 按钮置灰(最基础)
<button :disabled="isSubmitting" @click="handleSubmit">
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
⚠️ 前端方案的致命缺陷
- 用户可绕过(F12控制台直接调用接口)
- 断网重连后,请求被浏览器重放
- 多窗口/多设备操作完全无效
前端拦截仅用于提升体验,不能取代后端防御。
后端拦截方案(核心防线)
1 基于Token的防重复(最优解)
流程:
- 服务端生成唯一token(UUID)
- 前端提交时携带token
- 后端用Redis检验:
- 存在:删除token并执行业务
- 不存在:返回“重复提交”
代码实现(SpringBoot):
@PostMapping("/pay")
public Result pay(@RequestParam String token) {
Boolean success = redisTemplate.delete(token);
if (!Boolean.TRUE.equals(success)) {
return Result.error("订单正在处理,请勿重复提交");
}
// 执行支付逻辑
}
2 幂等表(适用于订单/转账)
CREATE TABLE idempotent (
id BIGINT AUTO_INCREMENT,
biz_key VARCHAR(128) UNIQUE, -- 业务唯一键(如订单号+操作)
PRIMARY KEY (id)
);
原理:插入幂等表时利用唯一索引,重复插入报错。
适用场景:强一致性的金融、支付业务。
3 数据库乐观锁
UPDATE inventory
SET stock = stock - 1
WHERE id = #{id} AND stock > 0; -- 查是否实际修改行数
if (updateRows == 0) {
throw new DuplicateSubmitException();
}
分布式场景下的终极方案(Redisson)
为什么需要分布式锁?
- 单体Redis的
delete-then-execute在并发下可能被绕过 - 节点扩容后,锁必须全局生效
基于Redisson的防重复锁
RLock lock = redissonClient.getFairLock("order:lock:" + orderId);
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
// 执行核心业务
} else {
throw new DuplicateSubmitException("请求正在处理");
}
} finally {
lock.unlock();
}
Redisson优势:
- 自动续期(看门狗机制)
- 公平锁防止饥饿
- 支持哨兵/集群模式
高并发下的避坑指南
❌ 常见错误1:Redis锁过期
场景:业务执行超过锁的过期时间,锁自动释放导致重复提交。
方案:设置合适的过期时间 + Redisson自动续期。
❌ 常见错误2:ABA问题
场景:A请求删除token后,B请求恰好插入一个相同token。
方案:使用Redis的SET NX EX原子命令,key为业务唯一标识。
✅ 降级策略:融断 + 限流
# Sentinel规则
resources:
submitOrder:
degrade:
count: 10
timeWindow: 60 # 60秒内失败10次则融断
Q&A常见问题
Q1:用户反复刷新页面怎么办?
- 前端:使用
history.replaceState()禁止后退刷新 - 后端:利用Token机制 + 前端请求携带上次的token
Q2:断网重连后请求被重放?
- 方案:为每个请求生成
requestId(前端生成) - 后端校验:同一requestId只执行一次
Q3:接口被恶意刷单?
- 出口:接入WAF(Web应用防火墙)
- 入口:限制同一IP/设备调用的频率
实战代码一键复用
自定义注解 + AOP(推荐)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String key() default ""; // SpEL表达式(如 #order.orderId)
long expire() default 10; // 锁过期时间
}
AOP切面:
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint pjp, Idempotent idempotent) {
String uniqueKey = parseKey(idempotent, pjp.getArgs());
RLock lock = redissonClient.getLock("idem:" + uniqueKey);
boolean locked = lock.tryLock(0, idempotent.expire(), TimeUnit.SECONDS);
if (!locked) {
throw new DuplicateSubmitException();
}
return pjp.proceed();
}
使用示例:
@Idempotent(key = "#order.id")
public String submitOrder(Order order) {
// 业务逻辑
}
构建拦截重复提交的黄金法则
- 前端:按钮置灰 + 防抖(10%作用)
- 后端:Token机制 + 幂等表(80%作用)
- 分布式:Redisson分布式锁(99.9%作用)
- 监控:接入秒级告警(发现重复立即定位)
没有绝对的防重复,只有体系化的防御,建议组合使用2~3层方案,并做好日志记录,以便事后追溯。
最后一问:如果用户通过F12把前端disabled属性改掉怎么办?
答案:依赖后端防御!前端只是“提示器”,后端才是“守门员”。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播