防重复提交设计
2026/1/21大约 3 分钟约 1023 字
防重复提交设计
适用场景:下单、支付、表单提交等需要防止用户重复点击的操作
1. 设计目标
- 防止用户快速多次点击导致重复提交
- 防止恶意脚本/抓包重放攻击
- 保证即使所有防御失效,数据也不会重复
2. 纵深防御体系
- 第1层:前端(防君子不防小人)- 按钮置灰 / Loading / 防抖节流
- 第2层:网关限流 - IP / 用户 / 接口级别限流
- 第3层:幂等 Token(核心防线)- Redis 原子校验,消费即删除
- 第4层:数据库唯一索引(最终兜底)- 唯一约束保证数据不重复
3. 各层详解
3.1 前端层(用户体验)
async function handleSubmit() {
if (loading) return; // 防重入
loading = true;
btn.disabled = true; // 按钮置灰
try {
await api.submit(data);
} finally {
loading = false;
btn.disabled = false;
}
}特点:
- 提升用户体验,避免误操作
- 可绕过:禁用 JS、脚本调用、抓包重放
3.2 网关限流(防刷)
# Sentinel / Nginx 限流配置示例
rate_limit:
- key: ${ip}:${uri}
qps: 10
- key: ${userId}:${uri}
qps: 5特点:
- 防止恶意刷接口
- 不防"正常用户的快速重复点击"
3.3 幂等 Token(核心)
流程图
1. 进入下单页
← 后端生成 Token,存 Redis(如 5 分钟过期)
← 返回 Token 给前端
2. 用户提交(携带 Token)
→ 后端 Redis 原子操作:存在则删除并放行
→ 不存在或已删除:返回"请勿重复提交"
3. 重复点击
→ Token 已消费 → 拒绝代码实现
// 1. 获取 Token(进入页面时调用)
@GetMapping("/order/token")
public String getOrderToken() {
String token = UUID.randomUUID().toString();
String key = "ORDER_TOKEN:" + getUserId();
redis.setex(key, 300, token); // 5分钟过期
return token;
}
// 2. 提交订单(携带 Token)
@PostMapping("/order/create")
public Order createOrder(@RequestParam String token, @RequestBody OrderDTO dto) {
String key = "ORDER_TOKEN:" + getUserId();
// Lua 脚本原子校验:存在则删除并返回1,否则返回0
String script =
"if redis.call('get',KEYS[1])==ARGV[1] then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long result = redis.eval(script, List.of(key), List.of(token));
if (result != 1) {
throw new BizException("请勿重复提交");
}
// Token 校验通过,继续创建订单
return orderService.create(dto);
}为什么用 Lua 脚本?
- 保证 GET + DEL 原子性
- 防止并发场景下两个请求都读到 Token 存在
3.4 数据库唯一索引(兜底)
-- 订单表唯一约束
CREATE UNIQUE INDEX uk_order_no ON t_order(order_no);
-- 或业务唯一键(如:用户+商品+时间窗口)
CREATE UNIQUE INDEX uk_user_product ON t_order(user_id, product_id, create_date);try {
orderMapper.insert(order);
} catch (DuplicateKeyException e) {
// 幂等处理:返回已有订单
return orderMapper.selectByOrderNo(order.getOrderNo());
}特点:
- 最终兜底,不可绕过
- 即使 Redis 宕机,数据库也能保证不重复
4. 不同场景的选择
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 下单/支付 | Token + 唯一索引 | 资金相关,必须严格 |
| 点赞/收藏 | Redis Set 判重 | 丢失可接受,性能优先 |
| 评论/发帖 | 唯一索引 + 限流 | 内容重要,但无资金风险 |
| 浏览量统计 | Redis INCR | 允许不精确 |
5. 常见问题
Q1: Token 存在哪里?
| 方案 | 优缺点 |
|---|---|
| Cookie/LocalStorage | 简单,但可被篡改 |
| 后端 Session | 有状态,分布式需共享 |
| Redis(推荐) | 无状态,支持分布式,可设过期 |
Q2: 分布式锁 vs 幂等 Token?
| 方案 | 适用场景 |
|---|---|
| 分布式锁 | 防止同一资源并发操作(如扣库存) |
| 幂等 Token | 防止同一用户重复提交(如下单) |
两者可以结合使用。
Q3: 幂等 Token 过期了怎么办?
用户在页面停留超过 Token 有效期后提交:
- 前端:监听 Token 过期,自动刷新
- 后端:Token 不存在时返回特定错误码,前端重新获取 Token
6. 面试总结
一句话:前端按钮置灰防君子,网关限流防刷,幂等 Token 防重复,唯一索引兜底。
核心要点:
- 前端:按钮禁用 + Loading(用户体验,可绕过)
- 网关:IP/用户/接口限流(防刷)
- 业务层:幂等 Token + Redis 原子校验(核心防线)
- 数据库:唯一索引(最终兜底,不可绕过)
