Skip to content

Q63 · MCP 调用如何保证并发? ​

用户 U9 问客服:“订单 A123 现在到哪了?‘运输中’又是什么意思?”客服需要查当前订单状态,还要读配送状态说明。两份信息互不依赖,可以同时读取,等都返回再回答。但如果同一时刻又有人发起“取消 A123”,读与写可能相互影响。这里说的“保证并发”,不是让 MCP 自动把所有任务无限制地并行,而是让适合并发的请求能被正确区分和汇合,并用应用与业务层的控制避免过载和竞态。

以下 U9、A123、B456 及时间都是假设示例。A123 的模拟物流结果是“运输中,9 月 26 日 10:00 到达苏州中转站”,说明资料把“运输中”解释为“包裹仍在配送链路中”。这两项都只读;涉及取消订单和退款的情节只用于说明风险,不代表真实业务政策。

先说明术语和符号 ​

**MCP(Model Context Protocol,模型上下文协议)**规定 AI 应用里的 Client 怎样向提供外部能力的 Server 发送请求、接收响应。并发指多个工作在同一段时间内处于进行中;它们可能由不同 CPU 同时执行,也可能在等待网络时交替推进。对本文的客服来说,两次查询同时在途就算并发,不能仅凭“发出了两次调用”推断服务器一定同时执行了两段代码。

名称含义A123 例子
Host / Client / ServerHost 是客服 AI 应用;Client 是它连接 MCP Server 的组件;Server 暴露可调用工具或资料客服 Host 通过 Client 接订单 Server
tools/call调用 Server 上某个工具的 MCP 方法查 A123 最新状态
resources/read读取一份资源的 MCP 方法读“运输中”的说明
JSON-RPC id请求与响应的对应编号;同一发送方未完成的请求不能撞号本例用 41、42 标两次查询
在途请求已发送、尚未得到最终响应的请求id=41 尚在查订单系统
Streamable HTTP / POSTMCP 的远端传输 / 一次 HTTP 请求发送方式每条 MCP 消息各用一个 POST
SSEServer-Sent Events,HTTP 响应中的事件流;可在同一请求的最终结果前送进度等消息某次耗时查询可在自己的响应流报进度
stdio本地子进程的标准输入输出传输;多条消息共享同一字节流本机 Client 给 Server 写两行请求
无会话 / stateless2026-07-28 规范不靠先前某次请求或连接保存协议上下文;每次请求带必要元数据id=42 不靠 id=41 的连接状态才能被理解
汇合 / barrier应用等所需结果到齐,再进入依赖它们的步骤状态和说明都到齐后才组织回答
限流 / 背压控制单位时间或同时进行的请求数 / 忙时让上游排队或拒绝订单系统一次最多接若干查询
竞态多个操作受完成先后影响,造成结果不一致查状态同时取消订单,读到的版本可能不同
幂等键业务操作的去重标识;同一意图重试应只有一次外部效果同一次退款申请重试不产生两笔退款
取消请求方表示不再等待并希望停止进行中的工作用户关闭提问或超时

请求 ID 不是订单号,也不是幂等键。id=41 只让 Client 知道哪份响应属于哪次调用;它不会让 A123 只查一次,更不会让“退款”自动避免重复执行。官方 2026-07-28 基础协议要求请求带字符串或整数 ID,同一发送方仍在等待响应的请求不能使用相同 ID,响应应带回对应 ID。MCP 2026-07-28 基础协议:Requests / Responses

哪些能力来自协议,哪些来自实现 ​

MCP 给并发提供可区分的请求响应结构。在 2026-07-28 版本中,Server 处理每条请求时应只依据该请求携带的协议版本、Client 能力和参数,不从同一连接的上一条请求推断上下文;同一 stdio 进程也可能交错承载不同任务。远端 Streamable HTTP 规定每条 JSON-RPC 消息各走一个 HTTP POST,Server 可为该请求返回单个 JSON,或返回仅属于该请求的 SSE 响应流。MCP 基础协议:Statelessness · MCP Streamable HTTP

这些规则让 Client 能同时保留多个在途请求,并把返回结果放回正确的位置。它们没有规定“每个 Server 必须同时跑 N 个工具”“先发的一定先完成”“同一订单的读写会自动互斥”。是否同时发起由 Host 决定;Server 的处理能力取决于 SDK、运行时、数据库连接池和自己写的逻辑。以官方 TypeScript SDK v2 的 HTTP 入口为例,createMcpHandler 为每个 HTTP 请求创建一个 Server 实例,便于把请求相关上下文放在实例内;这并不替共用的订单数据库或外部 API 建立业务隔离。MCP TypeScript SDK v2:Serve over HTTP

本地 stdio 是另一种形态:多条请求和响应共用一条标准输入输出流,由 JSON-RPC ID 对应响应。程序可以接收多个在途请求,但若 Handler 阻塞事件循环、SDK/Server 选择串行处理或数据库只有一个可用连接,实际吞吐仍可能接近串行。协议允许正确复用通道,不等于保证并行执行。MCP stdio 规范

两次独立读取如何安全汇合 ​

客服 Host 决定:get_order_status(A123) 与 resources/read(guide://shipping/status) 相互不依赖,都只读,所以在同一轮同时发起。此处示意 Client 给订单查询分配 id=41,给说明资源分配 id=42;实际 SDK 一般会管理这些协议 ID,业务代码无需手工写 41。图按左到右阅读:两张请求票据分别走自己的查询,结果按 ID 回到对应位置,Host 等两项都齐再组织答案。

客服 Host 对订单状态和配送说明发起独立请求,并按 ID 对应结果后汇合答复

示意时刻id=41:订单状态id=42:配送说明Host 能做什么
0 毫秒已发出,等待订单系统已发出,等待说明资源记录两项都在途
120 毫秒返回“运输中,9 月 26 日 10:00 到达苏州中转站”仍在途保存状态结果,暂不假定说明内容
170 毫秒已完成返回“运输中:包裹仍在配送链路中”两项齐全,可答“订单仍在配送链路中,最近到达苏州中转站”

这个时序只是假设。若 id=42 先返回,仍按它的 ID 放进“说明”槽位,等待 id=41;不能拿数组到达顺序猜哪个是订单结果。如果只完成一项就回答,会把“订单现在在哪”与“术语是什么意思”混成一个未经核对的结论。若资源读取失败,而它是回答用户第二个问题的必要依据,应说明这一部分暂时无法核实,或走受控降级路径,不让模型凭空解释。

Host 可以用类似下面的 JavaScript 函数片段表达“同时发起、全部落定、分别检查”。client 表示已经连接 Server 的 SDK Client;orderId 是业务订单号 A123,不是 JSON-RPC ID。Promise.allSettled 在两个 Promise 都成功或失败后给出各自结果;order 与 guide 分别对应固定位置的两个调用,status === 'fulfilled' 表示该调用得到返回,value.isError 表示工具虽有响应但报告了业务失败。ok 是本函数给上层的可用标志,content 和 contents 分别是工具和资源的返回内容。函数只有汇合与错误判断,没有实现鉴权、限流或取消,需要由调用它的应用与 Server 另行加入。

js
async function gatherOrderContext(client, orderId) {
  const [order, guide] = await Promise.allSettled([
    client.callTool({ name: 'get_order_status', arguments: { order_id: orderId } }),
    client.readResource({ uri: 'guide://shipping/status' }),
  ]);

  if (order.status !== 'fulfilled' || guide.status !== 'fulfilled') {
    return { ok: false, reason: '查询未全部完成' };
  }
  if (order.value.isError) {
    return { ok: false, reason: '订单查询失败' };
  }
  return { ok: true, order: order.value.content, guide: guide.value.contents };
}

给函数传入已连接的 Client 和 orderId='A123' 时,它会同时提交两项读取,等两项返回后得到 ok: true。若订单查询返回越权工具错误,ok 为 false;若资源请求抛出异常,也得到 false。这只是有依赖的最终回答之前可并发的读取段。项目中还要限制同时调用数,否则一次用户请求可扩散为几十次工具调用,压垮后端。官方 SDK 的 callTool、readResource 分别对应工具调用和资源读取;上面代码不依赖手工构造协议报文。MCP TypeScript SDK v2:Clients / Calling

竞态出现时,把顺序放回业务层 ​

考虑第二种情形:U9 一边问“现在到哪了”,一边点“取消 A123”。这两个请求虽然也能同时发出,却不能仅凭“都成功返回”推断答案与取消结果在同一时间点一致。例如查状态在取消前读到“运输中”,取消后订单系统变成“取消申请中”;客服若把旧状态当成取消后的状态,就给了用户过期信息。解决方法是按业务需要选择顺序:先完成取消并拿到最终结果,再重新查状态;或让订单系统返回版本号/更新时间,发现版本不一致就重查。MCP 请求 ID 只用于匹配各自的响应,不提供跨请求的事务快照或顺序锁。

更危险的是两个“发起退款”调用同时到达。两者都可能先读到“尚未退款”,然后各写一笔。Server 应把业务写入交给有事务或唯一约束的后端,使用针对同一退款意图的幂等键,重复调用返回同一业务结果;Host 也应避免对不确定是否已成功的写入盲目重试。幂等键的作用域、保存期限和是否允许复用由业务系统设计,不能拿 id=41 代替它。若两个操作有先后依赖,就串行;若互不依赖且无冲突,再考虑并发。

并发的只读请求也不必然共享一致快照:订单状态和配送说明可能来自不同更新时间。对时间敏感的解释,应保留各自来源与时间;需要强一致的判断,回到能提供同一版本或事务视图的业务系统。MCP 基础协议:Statelessness

超时、取消和限流怎样配合 ​

假设 U9 关闭页面,id=41 还在查订单。取消是停止继续等待并请求远端停工的信号,不是“已执行动作回滚”的证明。2026-07-28 规范的 stdio 传输要求 Client 发送引用请求 ID 的 notifications/cancelled;Streamable HTTP 对采用 SSE 响应流的请求,关闭该请求的流就是取消信号。Server 应尽快停下可取消的工作并释放资源;取消可能在查询刚完成后才到,因此 Client 还要容忍迟到响应。规范也建议为请求设置超时,超时后取消并停止等待。MCP 取消规范 · MCP Streamable HTTP:Cancellation

HTTP 请求也可能返回一个普通 JSON,而非 SSE 流。此时不要把“关闭 SSE”误当作所有 HTTP 响应形式都存在的步骤;由具体 Client/HTTP 实现处理本地中止和超时,并在业务上按结果未知处理已发出的写操作。尤其退款已经提交但响应在路上丢失时,再发一次前要先查业务状态或用相同幂等键重试。

即使都是只读,Host 也不该每次看到十个候选工具就同时打十个请求。可以给总请求数、每个用户、每个后端分别设在途上限。例如本系统自行规定“全局最多 20、同一用户最多 2、订单后端最多 5”,多出的进入有长度与等待时间限制的队列;队列满了便给出可理解的繁忙反馈。数字是容量规划示例,不是 MCP 标准。Server 还要限制数据库连接、CPU 工作和外部 API 速率,记录每种工具的延迟、超时与拒绝率。只读查询遇到临时失败可按预算做有退避的重试;写操作要先解决幂等与结果确认。背压从 Host 到 Server 再到订单系统逐层生效,否则协议能接收请求也不代表后端承受得住。

面试时可以这样回答 ​

MCP 的 2026-07-28 规范让每个请求独立携带上下文,JSON-RPC ID 对应请求和响应;Streamable HTTP 每条消息独立 POST,可返回单个 JSON 或该请求的 SSE 流。这样 Host 可以对互不依赖的只读工具或资源并发发起调用,按 ID 收结果,在需要两项数据的地方汇合。但协议不保证 Server 无限并行、先发先回、同一订单自动隔离。实现上我会先判断依赖关系,再给 Host 与后端设置并发上限、超时和错误处理;读写冲突交由业务事务、版本检查或串行控制,写操作用业务幂等键。取消是尽力停止,不能当回滚证明,并会测试乱序返回、超时、越权、限流和重复写入。

若追问“同一个 stdio 连接能否有两个在途请求”,可以回答:能按 ID 对应共享通道上的响应,但具体 SDK 和 Server 是否同时执行 Handler、吞吐多少,要通过实现与压测确认。若追问“SSE 是不是并发的前提”,可以回答:不是;每个 HTTP 消息本来就是独立 POST,Server 可以返回单个 JSON。SSE 是某条请求需要流式进度或响应时的承载方式。若追问“取消退款请求后能否立刻重新退款”,可以回答:不能仅凭取消信号判断前一次业务写入是否发生,先查状态或以同一业务幂等键安全重试。

参考资料 ​

最后更新2026-09-26
难度P1
频率high
阅读19 min
主题mcp / concurrency / json-rpc
觉得有帮助?把这个链接转给正在求职的朋友 · 用 Ctrl + K 全站搜索其它题