接口幂等性设计:确保重复请求安全处理的完整方案
接口幂等性是分布式系统设计中的关键特性,指多次执行相同请求时,系统最终状态与执行一次相同。这一特性可有效避免因网络重试、用户误操作等导致的数据重复或状态异常。本文将分析重复请求的成因,并提供从前端到后端的完整解决方案。
重复请求的常见场景
重复请求的产生可能源于客户端、网络或服务端,典型场景包括:
- 客户端操作失误:
- 用户快速点击提交按钮;
- 刷新页面、使用后退 / 前进按钮重复提交表单;
- 浏览器历史记录重复提交。
- 网络与重试机制:
- 网络延迟导致客户端未收到响应,触发重试;
- 分布式系统中的请求重试机制(如 Feign 重试、消息队列重试)。
- 系统设计缺陷:
- 前端未禁用重复提交;
- 后端未处理重复请求,导致重复写入数据库。
接口幂等性解决方案
确保幂等性需从前端防重和后端校验两方面入手,形成多层防护。
1. 前端防重:减少重复请求产生
前端是防止重复请求的第一道防线,通过限制用户操作和请求发送,从源头减少重复请求。
(1)禁用提交按钮
点击提交后立即禁用按钮,防止短时间内重复点击:
1 | <button id="submitBtn" onclick="submitForm()">提交</button> |
(2)使用重定向(Post/Redirect/Get 模式)
表单提交后通过重定向到结果页,避免用户刷新页面导致的重复提交:
- 提交表单后,后端处理请求并返回
302 重定向到成功页; - 刷新成功页时,浏览器仅重新请求结果页,而非表单提交接口。
1 |
|
(3)请求防抖与节流
通过防抖(Debounce)或节流(Throttle)限制请求频率:
1 | // 防抖:短时间内多次触发,仅执行最后一次 |
2. 后端校验:确保重复请求只执行一次
前端防重无法覆盖所有场景(如网络重试、绕过前端的恶意请求),需后端通过幂等设计确保安全性。
(1)基于 Token 的幂等校验(推荐)
核心思路:一次请求对应一个唯一 Token,后端通过 Token 判断是否为重复请求。
实现步骤:
生成 Token:
前端请求表单页时,后端生成唯一 Token(如 UUID),存入 Redis(或 Session),并返回给前端:1
2
3
4
5
6
7
8
9
10
public String getForm(Model model, HttpSession session) {
// 生成 Token
String token = UUID.randomUUID().toString();
// 存入 Session(或 Redis,分布式系统推荐)
session.setAttribute("requestToken", token);
// 传递给前端
model.addAttribute("token", token);
return "form";
}前端携带 Token 提交:
表单中添加隐藏字段存储 Token,提交时一并发送:1
2
3
4
5<form action="/api/submit" method="post">
<input type="hidden" name="token" value="${token}">
<!-- 其他表单字段 -->
<button type="submit">提交</button>
</form>后端校验 Token:
- 从请求中获取 Token,与存储的 Token 比对;
- 一致则处理请求,并删除存储的 Token(确保只能使用一次);
- 不一致则视为重复请求,直接返回结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String submit( String token, HttpSession session) {
// 从 Session 获取存储的 Token
String storedToken = (String) session.getAttribute("requestToken");
// 校验 Token
if (token == null || !token.equals(storedToken)) {
return "重复请求,已忽略";
}
// 校验通过,删除 Token(防止重复使用)
session.removeAttribute("requestToken");
// 处理业务逻辑(如创建订单)
orderService.create(...);
return "处理成功";
}
分布式系统适配:
- 用 Redis 替代 Session 存储 Token,确保多节点共享 Token;
- 设置 Token 过期时间(如 10 分钟),避免内存泄漏。
(2)数据库层幂等保障
即使前端和 Token 校验失效,数据库层仍需作为最后一道防线,防止重复写入。
① 唯一索引(适合 INSERT 操作)
对唯一标识字段(如订单号、用户 ID + 业务类型)创建唯一索引,重复插入时数据库会抛出 DuplicateKeyException,后端捕获并处理:
1 | -- 订单表:订单号唯一 |
1 |
|
② 乐观锁(适合 UPDATE 操作)
通过版本号(Version)控制更新,确保只有版本匹配时才执行更新,避免重复修改:
1 | -- 商品表:包含版本号字段 |
1 |
|
对应的 SQL 语句:
1 | <update id="reduceStock"> |
(3)基于状态机的幂等设计
对于有明确状态流转的业务(如订单状态:待支付→已支付→已完成),通过状态机约束重复操作:
- 定义状态流转规则(如 “已支付” 状态不能再次支付);
- 处理请求时检查当前状态,不符合流转规则则拒绝操作。
1 | public void payOrder(Long orderId) { |
各方案适用场景对比
| 方案 | 适用操作 | 优点 | 缺点 |
|---|---|---|---|
| 前端防重(禁用按钮、防抖) | 所有操作 | 简单直观,减少无效请求 | 无法防⽌绕过前端的恶意请求 |
| Token 校验 | 所有操作 | 通用性强,支持分布式 | 需额外存储 Token,增加开销 |
| 数据库唯一索引 | INSERT 操作 | 底层保障,可靠性高 | 仅适用于有唯一标识的场景 |
| 乐观锁 | UPDATE 操作 | 性能好,无锁竞争 | 需额外维护版本号字段 |
| 状态机 | 有状态流转的操作(如订单) | 符合业务逻辑,安全性高 | 适用场景有限 |
最佳实践
- 多层防护:结合前端防重 + 后端 Token 校验 + 数据库约束,形成完整防护链;
- 分布式适配:分布式系统中,用 Redis 存储 Token 和分布式锁,确保多节点一致性;
- 异常处理:重复请求应返回 “处理成功”(而非错误),避免客户端误解为失败而重试;
- 日志记录:记录重复请求的关键信息(如 Token、请求参数),便于问题排查