“Post请求为什么会发送两次”这个问题啊,听起来像是那种面试官坐那儿喝着咖啡,嘴一歪顺口问出来的问题。但你别说,这问题可真不是拿来唬人的,项目里真遇到过,不止一次。
我记得有次我们做一个内部系统的功能页面,前端提交数据之后,后端居然收到两次POST请求,结果数据库里出现了重复数据,用户还反馈说“怎么点击一次就新增了两条记录?”我当时心里想的是:这玩意儿又不是双击提交按钮,咋就来两次??
其实这个问题背后的坑还真不少,我们一起来扒一扒。
首先得明白一点:浏览器并不是傻子,发送POST请求不是心血来潮点两下,它也是有逻辑的。
最常见的情况是这样的:当用户提交表单之后,后端处理完请求并没有重定向,而是直接返回了一个结果页面。然后用户刷新这个页面时,浏览器为了“保住现场”,会再把上次的POST请求发一次——这就是传说中的:
刷新触发的POST重发(Post/Redirect/Get 模式未实现)
如果你有几年Web开发经验,肯定听说过PRG模式,Post-Redirect-Get。这个模式其实就是为了解决用户刷新页面导致重复提交的问题而发明的。当你提交完表单后,服务器不是直接返回结果,而是302重定向到一个新的URL,浏览器再发起GET请求去展示结果页面。这样,即便用户按F5刷新,刷的是GET请求,而不是POST,就不会重复提交数据。
那时候我们团队的做法就是加上重定向。看个Spring MVC里最简单的例子:
@PostMapping("/save")
public String saveUser(User user) {
userService.save(user); // 保存用户
return "redirect:/success"; // 重定向,防止重复提交
}
这段代码看起来没啥,关键是那个redirect:
,你要是改成返回视图名,那刷新就会重复提交POST请求。
然后还有一种情况也很坑人,那就是前端写了“防抖”逻辑,结果写得不彻底。比如Vue项目里加了个按钮提交后禁用的逻辑,表面上是禁掉了,但在某些网络条件差的时候,用户以为没提交成功,手贱点两次,结果就发了两个POST。这其实是前端锅,但后端必须兜底啊。
我们后来在项目里统一加了“幂等校验”,啥意思?同一个用户、同一时间、同一个请求参数,我们给它生成一个唯一的Token或者Hash,第一次来处理,第二次来就直接拒绝。可以这样:
public boolean isDuplicateRequest(HttpServletRequest request) {
String key = generateKeyFromRequest(request); // 从参数和用户信息生成唯一Key
if (redis.exists(key)) {
return true;
}
redis.set(key, 1, 30, TimeUnit.SECONDS); // 设置30秒有效期
return false;
}
这段逻辑是“后端反击”的典范,不管你前端点多少次,我只处理第一次,后面的一律打回去。
还有更阴间的一种情况,是浏览器自动重发。比如网络不稳定,第一次POST请求没收到完整响应,浏览器以为失败了,就偷偷又发了一次。这种情况在移动网络、隧道代理、VPN中都可能出现。我们线上有一次就是被某款国产浏览器坑了,发了两次POST,用户下了两次订单,关键还都扣了钱……🫠
后面分析Nginx日志才看到,确实是两次POST,第二次来的时候用户压根儿没操作,只是浏览器自己又发了一次。这种情况更得靠幂等机制救命。
那你说,是不是所有POST请求都可能重复发?其实不是。
通常这几种场景最常见:
刷新导致的重复POST
用户重复点击按钮
浏览器重试机制触发
JS框架或插件bug
前端用了fetch但没设置
keepalive:false
时反复触发请求Nginx或中间代理层重发请求
我们项目里曾经因为Nginx配置问题,出现过POST请求超时后被Nginx自动重试的奇葩情况。你看着请求是发了一次,其实Nginx后面帮你补了一枪...
Nginx默认对GET请求有重试机制,POST默认不重试。但你配置了某些代理模块,比如proxy_next_upstream
,就可能误杀POST也重发了。要规避,可以加这类配置:
proxy_next_upstream off;
这个坑真的是,掉进去就头秃……
还有人说“你不如把所有POST都改成GET”,我只能说,哥你是看不起HTTP协议吗?POST是有状态改变含义的,请求体里通常带着重要的参数,改成GET那就全暴露在URL上了,安全性也有问题。
所以,根本办法还是两个方向:
前端层防止重复触发(按钮禁用、防抖)
后端层做幂等设计(Token、Hash、数据库唯一索引兜底)
尤其是金融、电商这类高敏感度系统,幂等性设计就是生命线。我们做支付接口的时候,光一个“支付成功回调”就被发了五次,后来统一用订单号+状态锁死,如果已经是“支付成功”,就不再变动。
讲真,这种面试题,看似简单,其实背后涉及HTTP协议、前后端协作、幂等性设计、代理层配置等多个方面。你要真说清楚了,面试官基本就知道你不光是“码农”,是真的写过线上服务的。
你要说解决方式,简单粗暴点也行:加redirect、防重复提交、接口幂等处理。但你要真懂为什么会发生,那面试官就得点头:“嗯,真干过活”。
最后我留个坑给你们想想:你们系统里所有POST接口,幂等性都做好了吗?尤其是那种操作频率不高但影响很大的接口,比如“删除账号”“转账操作”“重置密码”……可千万别侥幸。系统出事那天,你在那儿debug请求日志的时候,用户已经打电话投诉到领导办公室了。