news 2026/6/14 22:19:59

面试官:post 为什么会发送两次请求?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
面试官:post 为什么会发送两次请求?

面试官:post 为什么会发送两次请求?

——这个问题真的是面试高频,又容易翻车。

你想象一下哈。

你在面试,写了个很标准的 Spring Boot Controller:

@RestController @RequestMapping("/user") public class UserController { @PostMapping("/save") public String save(@RequestBody UserDTO user) { System.out.println("save user = " + user); return "ok"; } }

本地调试的时候,你打开浏览器的 Network 面板,或者看后端日志,发现一个很诡异的事:

你明明只点了一次“保存”, 结果:

  • 浏览器 Network 里出现了两条记录

  • 后端日志里也像是执行了两次

这时候面试官一句话丢过来:

“你说说,POST 为啥会发两次?”

如果只回答一句“网络不好重试了”,基本直接凉。这个问题背后,其实大概就那几种情况,只要搞清楚了,回答起来就很顺。

场景一:其实只算一次 —— CORS 预检把你吓到了

最常见的误会:OPTIONS + POST 被你当成“POST 调了两次”。

浏览器在跨域、且请求“比较复杂”的时候,会先发一个OPTIONS请求问问后端:“哥们,这个真正的 POST 我能不能发?”

流程是这样的:

  1. 浏览器先发:OPTIONS /user/save

  2. 后端返回一堆Access-Control-Allow-*

  3. 浏览器确认“没问题”,再发真正的:POST /user/save

所以 Network 面板里会看到两条记录,但真正的业务 Controller 只会命中一次(OPTIONS 通常不会走你的业务逻辑)。

在 Spring Boot 里,如果你打开了全局 CORS,大概长这样:

@Configuration publicclass CorsConfig { @Bean public WebMvcConfigurer corsConfigurer() { returnnew WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:3000") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true); } }; } }

想确认是不是这个原因,很简单:

  • 看 Network 里的 Method,是不是一条 OPTIONS,一条 POST

  • 后端给 Controller 打个日志,只会进一次就是 CORS 预检

面试的时候可以怎么说?

很多同学看到浏览器有两条记录,就以为 POST 调了两次,其实一条是 CORS 预检的 OPTIONS,一条才是真正的 POST,这种场景业务只会执行一次。

这一句说清楚,面试官一般会点点头。

场景二:真的发了两次 —— 重定向搞的鬼

第二种非常常见的原因,是重定向(redirect)

举个很典型的坑:

你有个接口/order/create,没登录时会被网关或者 Spring Security 拦截,返回 302 跳到/login

流程变成:

  1. 浏览器发:POST /order/create

  2. 服务端返回:302 Location: /login

  3. 浏览器自动再发一个:GET /login

这时候你在 Network 里看到两条记录:

  • 一条POST /order/create 302

  • 一条GET /login 200

严格说只有一个 POST,但如果是“POST -> POST”的重定向,就真有可能业务被打到两次。

比如你在 Nginx 里写了比较奇怪的 rewrite,把/order重写成了/order/,或者从 HTTP 重定向到 HTTPS,配置不当的时候,会出现:

  1. POST http://xxx/api/order/create

  2. 301 跳转到https://xxx/api/order/create

  3. 浏览器再次发请求

有些浏览器/代理在 301/302 时会把 POST 变成 GET,有些在 307/308 会保留原来的方法,这就很有讲头了。

你可以顺手提一句:

如果服务端返回的是 307 / 308,浏览器会保留原来的方法和 body,这时候就可能出现两次 POST 请求,所以线上做跳转时要小心这些状态码的使用。

场景三:客户端 / 网关的“好心重试”

第三种,就是各种“自动重试机制”。

常见几类:

  1. 网关 / 负载均衡

    • Nginx:proxy_next_upstream配置了超时/错误重试

    • 某些 API 网关默认帮你重试一次

  2. HTTP 客户端自己重试

    • OkHttp 默认就有retryOnConnectionFailure(true)

    • Feign 可以配置Retryer

    • 自己封装的 RestTemplate 可能也加了重试逻辑

一个简单的 Feign 重试配置示例:

@Configuration public class FeignRetryConfig { @Bean public Retryer feignRetryer() { // period=100ms, maxPeriod=1s, maxAttempts=3 return new Retryer.Default(100, TimeUnit.SECONDS.toMillis(1), 3); } }

如果下游服务第一次稍微慢一点,或者偶发抖动,Feign 觉得“有点不太对,我再试一次”, 于是你后端就看到了两次 POST。

这类情况的特点:

  • 两次请求的 body 完全一样

  • 时间间隔非常短(几十毫秒到几百毫秒)

  • 第一次可能是超时 / 5xx,第二次成功

排查的时候,除了应用日志,一定要看Nginx / 网关日志,很多“鬼畜请求”都藏在那里面。

场景四:前端真点了两次(或帮你点了两次)

这个就比较接地气了。

最常见的几个:

  1. 用户手速太快,按钮双击

  2. 回车提交 + 点击提交

  3. 页面做了自动重试,或者某种“点击即重发”的逻辑

  4. 前端没做节流/防抖,输入框变更就发 POST

最朴素的示例(反面教材):

<button id="submitBtn">提交</button> <script> document.getElementById('submitBtn').onclick = function () { fetch('/user/save', { method: 'POST', body: JSON.stringify({ name: 'Tom' }), headers: { 'Content-Type': 'application/json' } }); }; </script>

啥防护也没有,用户双击一下,直接两次 POST,后端根本没法分辨是不是同一次操作。

稍微规范一点的写法会在第一次点击后把按钮禁用:

const btn = document.getElementById('submitBtn'); btn.onclick = function () { if (btn.disabled) { return; } btn.disabled = true; fetch('/user/save', { method: 'POST', body: JSON.stringify({ name: 'Tom' }), headers: { 'Content-Type': 'application/json' } }).finally(() => { btn.disabled = false; // 看业务决定要不要恢复 }); };

面试的时候,可以顺带提一句“前端也要配合做防重复提交”,给人感觉你是站在全链路视角看问题的。

核心补救:POST 要配上“幂等性保险”

上面这些情况,有的是“看起来发了两次,其实没问题”(比如 CORS), 有的是真发送了两次。

那真正线上要紧的是:就算发了两次,也不能让业务乱套。

这就绕不过一个词:幂等性—— 同一个请求重复执行多次,结果应该是一样的。

POST 默认不是幂等的,所以要自己加“保险”。

比较常见的几种做法:

1. 用业务唯一键做幂等

比如支付、下单这种,通常都有一个业务唯一号,比如orderNo

服务端可以用 Redis / 数据库做一次“抢占”,谁抢到了谁执行:

@Service publicclass PayService { privatestaticfinal String IDEMPOTENT_KEY_PREFIX = "pay:order:"; @Resource private StringRedisTemplate stringRedisTemplate; public void pay(String orderNo) { String key = IDEMPOTENT_KEY_PREFIX + orderNo; // setIfAbsent = true 说明是第一次处理 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(key, "1", Duration.ofMinutes(10)); if (Boolean.FALSE.equals(success)) { // 说明已经处理过这笔支付,直接返回,不再重复扣款 System.out.println("orderNo = " + orderNo + " 已处理过,拒绝重复支付"); return; } // 真正的扣款逻辑 doPay(orderNo); } private void doPay(String orderNo) { // 调用三方支付、更新订单状态、落库等等…… System.out.println("执行真正的支付逻辑, orderNo = " + orderNo); } }

哪怕网关、客户端帮你把同一笔支付请求重试了好几次,只要orderNo相同,这个方法也只会真正执行一次。

2. 用一次性“幂等 token”

再举个更“通用”的 POST 场景,可能没天然的业务唯一键,可以走幂等 token方案:

  • 前端在提交前先向后端要一个token

  • 真正提交时把token带上

  • 后端把token当 key,setIfAbsent一次

  • 重复提交时setIfAbsent失败,直接返回“重复请求”

伪代码示意:

@RestController @RequestMapping("/idempotent") publicclass IdempotentController { @Resource private StringRedisTemplate stringRedisTemplate; @GetMapping("/token") public String generateToken() { String token = UUID.randomUUID().toString(); stringRedisTemplate.opsForValue() .set("idem:token:" + token, "1", Duration.ofMinutes(5)); return token; } @PostMapping("/submit") public String submit(@RequestHeader("Idempotent-Token") String token, @RequestBody SubmitDTO dto) { String key = "idem:token:" + token; Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(key, "used", Duration.ofMinutes(5)); if (Boolean.FALSE.equals(success)) { return"重复请求"; } // 真正的业务逻辑 handleBusiness(dto); return"ok"; } private void handleBusiness(SubmitDTO dto) { System.out.println("处理业务: " + dto); } }

这样哪怕浏览器帮你“多按了几次”,同一个 token 也只能成功一次。

真遇到线上“POST 两次”,怎么排查?

如果是实战,而不是纸上谈兵,我一般会这么干(你可以照着改成自己的话术):

  1. 先看浏览器 Network

    • Method 是 OPTIONS + POST,还是两条 POST?

    • 有没有 301/302/307/308 这种跳转?

    • 两次请求的 URL、参数、body 是否完全一样?

  2. 看网关 / Nginx 日志

    • 一条请求是否被转发到了多个上游?

    • 有没有重试 / upstream timed out 之类的记录?

  3. 看应用层日志

    比如用 Spring 的过滤器简单加个 requestId:

    @Component publicclass LogFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String requestId = UUID.randomUUID().toString().replace("-", ""); MDC.put("requestId", requestId); try { HttpServletRequest req = (HttpServletRequest) request; System.out.println("requestId=" + requestId + ", " + req.getMethod() + " " + req.getRequestURI()); chain.doFilter(request, response); } finally { MDC.remove("requestId"); } } }
    • 给每个请求加一个requestId或 traceId,打印在入口和核心业务逻辑里

    • 确认业务代码是不是确实走了两遍

  4. 最后看配置

    • Feign / OkHttp / RestTemplate 有没有配置重试

    • 网关有没有“自动重试一次”的选项

    • 前端有没有防重复提交

排查完,你基本可以非常有底气地告诉别人:这两次 POST 到底从哪儿来的。

https://mp.weixin.qq.com/s/9vVePfiEDeE_Ef3Qgx3Cvw

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 12:59:17

自主软件工程突破:IQuest-Coder-V1生产部署详解

自主软件工程突破&#xff1a;IQuest-Coder-V1生产部署详解 IQuest-Coder-V1-40B-Instruct 是一款专为现代软件工程与竞技编程场景打造的大型语言模型&#xff0c;具备强大的代码生成、推理与工具调用能力。它不仅能够理解复杂的编程逻辑&#xff0c;还能在真实开发流程中模拟…

作者头像 李华
网站建设 2026/6/14 4:53:47

3款神器彻底解决数据中心机柜管理难题

3款神器彻底解决数据中心机柜管理难题 【免费下载链接】awesome-sysadmin A curated list of amazingly awesome open-source sysadmin resources. 项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-sysadmin 还在为混乱的机柜布局而烦恼吗&#xff1f;每次设…

作者头像 李华
网站建设 2026/6/14 4:16:57

Qwen3-Embedding-0.6B显存占用高?轻量化部署优化实战教程

Qwen3-Embedding-0.6B显存占用高&#xff1f;轻量化部署优化实战教程 在实际AI模型部署中&#xff0c;显存资源往往是制约服务上线的关键瓶颈。尤其是像Qwen3-Embedding-0.6B这类参数量达到6亿的嵌入模型&#xff0c;虽然性能强大&#xff0c;但在边缘设备或资源受限环境中运行…

作者头像 李华
网站建设 2026/6/10 15:23:01

GPEN与其他开源修复工具(GFPGAN)功能差异全面对比

GPEN与其他开源修复工具&#xff08;GFPGAN&#xff09;功能差异全面对比 1. 为什么需要对比GPEN和GFPGAN&#xff1f; 你是不是也遇到过这样的问题&#xff1a;老照片发黄模糊、手机拍的人像噪点多、证件照不够清晰&#xff0c;想修图又怕修得不自然&#xff1f;市面上确实有…

作者头像 李华
网站建设 2026/6/14 10:51:02

语音情感识别新玩法!用Emotion2Vec+做心理辅导辅助工具

语音情感识别新玩法&#xff01;用Emotion2Vec做心理辅导辅助工具 在心理咨询和心理健康服务中&#xff0c;情绪的准确识别是关键的第一步。传统的心理评估依赖于面对面交流、问卷调查或主观观察&#xff0c;耗时且容易受主观因素影响。如今&#xff0c;随着AI技术的发展&…

作者头像 李华
网站建设 2026/6/10 16:00:37

青龙面板自动化脚本库:新手快速上手完整指南

青龙面板自动化脚本库&#xff1a;新手快速上手完整指南 【免费下载链接】QLScriptPublic 青龙面板脚本公共仓库 项目地址: https://gitcode.com/GitHub_Trending/ql/QLScriptPublic 想要轻松实现多个平台的自动签到任务吗&#xff1f;QLScriptPublic脚本库为你提供了完…

作者头像 李华