通用面试:HTTP 428 Precondition Required 如何避免丢失更新?
题干与适用场景
两个客户端同时编辑同一份文档。服务端要求更新请求携带条件首部:缺少条件时返回 428,条件存在但不再匹配时返回 412。请说明 ETag、If-Match、缓存和重试如何配合,避免后写入覆盖先写入。
面试官考察点
- 是否理解 428 是“必须带条件”的策略要求,412 是条件已评估但失败。
- 能否用强 ETag 和 If-Match 建立乐观并发控制。
- 能否设计读取、编辑、提交、冲突展示和重新合并流程。
- 能否处理缓存、幂等性、审计和异常重试边界。
回答前需要澄清的问题
- 哪些资源和写方法必须条件化,是否允许
If-Match: *? - ETag 是强验证器还是弱验证器,是否随每次表示变化?
- 客户端遇到 428、412、404 或 409 时分别怎么处理?
- 冲突需要字段级合并、人工选择还是直接放弃?
- 是否有缓存、版本号、幂等键和审计要求?
30 秒回答框架
客户端先 GET 资源并保存 ETag,编辑后用 If-Match 提交。缺少条件时服务端返回 428,表示客户端必须补条件;条件不匹配时返回 412,表示资源已被修改。客户端应重新读取、展示差异并合并后再提交,不能盲目覆盖。服务端用强 ETag 原子比较并记录版本和审计,缓存必须正确处理条件请求。
分步骤深入解答
第一步:区分 428 与 412
428 来自服务端策略:请求没有所需的前置条件;412 表示请求带了条件,但当前资源状态不满足该条件。两者都不是让客户端直接重试原请求的信号。
第二步:生成资源版本
服务端为表示生成强 ETag,内容或受保护的版本变化时更新它。GET 响应返回 ETag,客户端把它保存为编辑基线,而不是只保存本地时间戳。
第三步:提交 If-Match
客户端在 PUT、PATCH 或其他有副作用的更新中发送 If-Match。服务端在应用写入前原子比较当前 ETag;匹配才执行,否则返回 412 并保持资源不变。
GET /documents/42
ETag: "v17"
PUT /documents/42
If-Match: "v17"第四步:处理冲突
收到 412 后重新 GET,向用户展示服务器版本与本地改动的差异。字段可安全合并时生成新版本;无法自动合并时要求用户选择,不能把旧的 If-Match 再发一次。
第五步:设计缓存行为
条件 GET 可以用 If-None-Match 获得 304,但写入保护使用 If-Match。缓存层不能把旧 ETag 当成当前版本,也不能缓存会泄露资源状态的错误响应。
第六步:处理异常与重试
网络超时后不要直接重放非幂等写入;先查询资源版本或使用幂等键确认结果。428 需要补条件,412 需要重新读取和合并,重试逻辑必须区分这两类动作。
第七步:记录与验证
记录版本、ETag、冲突次数、自动合并率和人工放弃率。用并发更新测试验证“先到者成功、后到者 412”,并确认审计记录能还原每次版本变化。
高质量示范回答
我会让 GET 返回强 ETag,例如 "v17",客户端编辑时保留它。PUT 没有 If-Match 就返回 428,明确提示必须使用条件更新;带有旧 ETag 则在原子比较失败时返回 412。客户端收到 412 后重新 GET,展示服务器与本地差异,合并后用新 ETag 再提交,绝不重复发送旧条件。缓存把 If-None-Match/304 用于读取优化,不能替代写保护。超时的写请求先查询版本或用幂等键确认结果,并监控冲突率和自动合并率。
常见错误
- 把 428 和 412 都当成服务暂时不可用并无限重试。
- 使用客户端时间戳代替强 ETag 的原子比较。
- 收到 412 后直接覆盖服务器版本。
- 混淆 If-None-Match 的缓存验证与 If-Match 的写入保护。
- 超时后无条件重放有副作用的写请求。
追问及应对
追问一:为什么不用 Last-Modified?
时间戳精度和时钟问题可能让两个版本看起来相同;强 ETag 直接绑定表示版本,更适合防止丢失更新。
追问二:If-Match: * 什么时候有用?
它表达“资源必须存在”或避免在未知版本上创建/覆盖,具体语义要由 API 文档规定;不能把它当成无条件写入。
追问三:412 与 409 有什么区别?
412 是 HTTP 条件首部未满足;409 表示请求与当前资源状态发生业务冲突。接口可以同时使用,但响应应说明可恢复动作。
追问四:如何做字段级合并?
读取共同基线、服务器版本和本地版本,按字段策略合并;对同一字段的冲突交给用户,并用新 ETag 重新提交。
追问五:缓存返回旧 ETag 怎么办?
对编辑前的 GET 使用适当的缓存控制或 revalidation,提交失败时强制读取源站;服务端仍以当前版本原子比较为准。
追问六:怎样测试并发安全?
让两个客户端读取同一 ETag 并同时写入,验证只有一个成功、另一个收到 412;再覆盖重试、超时、缓存和审计链路。