题干与适用场景
你负责一个团队资料 API。客户端可能只修改 displayName,也可能提交完整资料;移动端网络不稳定会重试。请解释何时使用 PUT 或 PATCH,如何处理字段缺失与 null、并发冲突、原子性、幂等重试和兼容性。
Greenroom 的 2026 后端面试题单把 PUT 与 PATCH 的区别、幂等性和方法选择列为 API 题;RFC 5789 规定 PUT 的请求实体代表资源的新完整版本,而 PATCH 的实体是一组应用到当前资源的修改指令。本文不绑定具体公司。
面试官考察点
普通回答只背“PUT 全量、PATCH 增量”。强回答会先定义资源契约,再说明缺失字段是否保持原值、null 是否清除字段、更新是否需要 If-Match,以及失败时是否保证整份变更都不生效。面试官还会追问重复请求、响应丢失、未知字段、审计事件和旧客户端。
核心信号是能把 HTTP 方法语义落到数据库更新和并发控制,而不是把 PATCH 当成“更省流量的 PUT”。
回答前需要澄清的问题
- PUT 是否允许创建? 本题只更新已存在的团队资料;若允许以稳定 URI 创建,需要明确资源所有权和重复请求语义。
- 客户端发送的是完整资源还是变更文档? 完整资源用 PUT;字段操作或部分表示用 PATCH。
- 缺失字段和
null分别表示什么? 本题约定 PATCH 中缺失表示保持原值,null表示清除可空字段;不可空字段拒绝。 - 并发更新能否覆盖? 本题不接受静默覆盖,要求
ETag/If-Match或数据库版本条件。 - 是否需要触发副作用? 资料更新后的搜索索引、审计事件和通知必须与成功状态一致,异步副作用不能假装属于 HTTP 原子性。
30 秒回答框架
“我把 PUT 定义为用完整表示替换资源,用于客户端拥有全量快照的场景;PATCH 传递部分修改,用于只改 displayName 这类操作。PATCH 必须明确缺失和 null 的含义,并在资源版本不匹配时返回冲突,避免旧表单覆盖新资料。服务端在一个数据库事务中原子应用变更,成功后发出带版本的异步事件;客户端用同一个可重试请求语义和 If-Match 重试。若变更是命令而非资源修改,我会另设动作端点,不滥用 PATCH。”
分步骤深入解答
第一步:把两种方法写成资源契约
| 维度 | PUT | PATCH |
|---|---|---|
| 请求含义 | 请求体是资源的新完整表示 | 请求体是应用到当前资源的修改指令或部分表示 |
| 未出现字段 | 通常代表客户端提供的完整状态,不应静默保留旧字段 | 必须明确是保持原值还是非法 |
| 幂等性 | 相同表示重复应用应得到同一资源状态 | 规范不保证幂等,但特定 PATCH 文档可以设计成幂等 |
| 典型场景 | 同步完整编辑器快照、替换配置 | 修改单个字段、JSON Merge Patch 或 JSON Patch |
PUT 的关键不是“请求体更大”,而是客户端声明了资源完整状态。若客户端只知道部分字段,却发送 PUT,服务端可能把未携带字段当成删除或默认值。PATCH 的关键不是“永远安全”,它仍然可能触发校验、权限和其他资源副作用。
第二步:明确 PATCH 文档和字段三态
PATCH /v1/teams/t_123/profile HTTP/1.1
Content-Type: application/merge-patch+json
If-Match: "profile-v17"
{"displayName":"Design Platform","avatarUrl":null}本题采用 JSON Merge Patch 风格:displayName 被替换,avatarUrl: null 清除可空字段,未出现的字段保持不变。若业务需要数组元素级操作、移动或条件测试,改用 JSON Patch 风格的操作列表,并限制允许的路径。
服务端先解析文档,再按字段白名单、类型、长度、权限和业务不变量校验;禁止把客户端任意 JSON 路径直接映射到数据库列。未知字段可以严格拒绝,也可以在版本契约中忽略,但必须在文档中固定,否则不同客户端会得到不同结果。
第三步:用版本条件阻止静默覆盖
GET /v1/teams/t_123/profile HTTP/1.1
ETag: "profile-v17"
PATCH /v1/teams/t_123/profile HTTP/1.1
If-Match: "profile-v17"
Content-Type: application/merge-patch+json
{"displayName":"Design Platform"}数据库更新带版本条件:只有当前版本仍为 17 才写入新值并递增到 18。条件不满足返回 412 Precondition Failed,客户端重新读取、展示冲突或重新生成补丁;它不能把旧版本强行覆盖。若请求缺少必需的 If-Match,可以用 428 Precondition Required 强制调用方声明并发策略。
PATCH 还必须整份原子应用:字段 A 成功、字段 B 失败时不能留下半个变更。数据库事务、校验阶段和唯一约束共同决定是否提交;副作用事件应在提交后由 outbox 或等价机制异步发布,并携带资源版本。
第四步:处理重复请求和未知结果
PUT 的重复请求只要完整表示相同,最终资源状态相同。PATCH 只有在操作本身可重复时才有同样性质:设置 displayName 是幂等的,increment seats by 1 不是。非幂等 PATCH 需要请求 ID、条件版本或改成表达目标状态的操作。
网络断开时,客户端不知道服务端是否已提交。客户端应使用稳定请求 ID,服务端记录请求指纹与结果,或用 If-Match 和目标状态重试;不能无条件重放会追加副作用的补丁。服务端返回成功后再由事件消费者处理索引和通知,重复事件由事件 ID 去重。
第五步:决定何时不用 PUT/PATCH
“发布团队资料”“重新计算权限”“发送邀请”是命令,不是资源表示的替换或局部修改。为它们设计 POST /profile:publish 等动作端点更能表达权限、审计、重试和异步状态。若把命令伪装成 PATCH,客户端会误以为只是在修改字段,难以理解重复执行和副作用。
高竞争或跨资源事务也可能更适合领域命令:例如把成员角色从 editor 变成 owner 需要检查名额和审计规则,单纯 PATCH 一个字符串不足以表达业务不变量。
高质量示范回答
“我会同时提供 PUT 和 PATCH,但让它们承担不同契约。PUT 接受完整的团队资料表示,客户端省略字段属于契约错误或明确的默认值,不会被当成‘保持不变’。PATCH 接受受限的 Merge Patch,只允许修改白名单字段;缺失字段保持不变,null 只对可空字段表示清除。
“两者都要配合 ETag 和 If-Match。数据库用版本条件更新,版本不匹配返回 412,避免旧客户端覆盖新资料。PATCH 文档先完整校验,再在单事务中应用;提交后通过 outbox 发布带版本的索引事件。设置字段的 PATCH 可以幂等,递增这类操作必须使用请求 ID、条件版本或改成‘设置目标值’。发布、邀请等有明显副作用的动作使用 POST 动作端点。最后我会测试重复请求、响应丢失、字段缺失/null、并发冲突、未知字段、部分失败和旧客户端兼容。”
常见错误
- 错误表现 → 把 PUT 解释成“更新任意几个字段” → 失败原因 → 调用方无法知道遗漏字段是否会被清除 → 修正方法 → 把 PUT 契约固定为完整表示,局部修改使用 PATCH。
- 错误表现 → 说 PATCH 天生幂等 → 失败原因 → RFC 5789 不保证 PATCH 幂等,增量操作重复会改变结果 → 修正方法 → 只把设置目标状态的补丁视为可重复,并为其他操作增加条件或请求去重。
- 错误表现 → 直接把 JSON 键映射到数据库列 → 失败原因 → 绕过字段权限、类型校验和跨字段不变量 → 修正方法 → 使用字段白名单和显式领域校验。
- 错误表现 → 版本冲突时最后写入获胜 → 失败原因 → 旧表单会静默覆盖新数据 → 修正方法 → 使用
If-Match与版本条件,冲突返回 412。 - 错误表现 → 数据库提交后立刻同步调用搜索服务 → 失败原因 → 响应丢失或进程崩溃会让状态与索引分叉 → 修正方法 → 事务内写 outbox,提交后异步投递并按事件 ID 去重。
追问及应对
如果产品要求 PATCH 缺失字段也清除,怎么办?
这会把 PATCH 变成另一种完整表示契约,容易与 PUT 混淆。可以改用 PUT,或明确采用“字段集合替换”语义并单独命名媒体类型;关键是让客户端知道未发送字段的后果。
如果两个客户端都基于版本 17 修改不同字段呢?
默认仍返回一个 412,让客户端合并后重试,因为服务端无法自动证明两项修改不会冲突。只有业务明确允许字段级合并,才能按字段版本或 JSON Patch 的测试操作安全合并。
如果 PATCH 触发计费或通知怎么办?
把资源更新和副作用分成两个可审计步骤:资源事务提交后写 outbox,消费者按事件 ID 幂等处理。若副作用本身必须由用户显式触发,改成独立 POST 命令并返回异步操作状态。