题目与场景
API 为文档返回版本验证器,多个客户端可以并发读写;要求拒绝过期写入,同时保持接口可重试且可观测。
面试官考察什么
- 区分表示缓存与写入前置条件。
- 选择强 ETag,并在变更方法上执行
If-Match。 - 过期版本返回 412,并定义安全的客户端恢复路径。
作答前的澄清问题
- ETag 代表精确存储表示,还是只代表弱语义版本?
- PUT、PATCH、DELETE,还是所有写操作都必须带前置条件?
- 客户端是否自动合并字段,还是必须由用户解决冲突?
- 重试是否经过负载均衡 API 和同一个事务型数据存储?
30 秒回答框架
我会在每个可编辑表示上返回强 ETag。客户端在 PUT、PATCH 或 DELETE 中发送 If-Match。服务端在同一事务内比较它并执行更新;版本不匹配则返回 412,不产生副作用。响应应提供当前表示或重新获取信号,同时记录冲突和缺少前置条件的指标。客户端重新获取、明确合并,再用新 ETag 重试。
分步深挖
1. 生成验证器
GET 返回文档和由规范化存储版本生成的 ETag。数据库修订号通常比对大载荷哈希更简单,只要相关表示每次变化都会更新。If-Match 保护精确写入状态,应使用强验证器。
2. 原子执行前置条件
更新必须在同一个条件操作中检查期望版本并写入新版本。概念 SQL 如下:
UPDATE documents
SET body = :new_body, version = version + 1
WHERE id = :id AND version = :expected_version;影响行数为零时返回 412,不执行副作用。先在应用内检查 ETag、稍后再写入会留下检查与提交之间的竞态。
3. 区分 412 与其他失败
412 表示提供的前置条件为假,是并发冲突;它不是 JSON 格式错误,也不是认证失败。请求结构无效用 400,认证或权限问题用 401/403,资源按接口策略不可用时用 404。这样客户端才能选择重新获取合并,而不是统一重试。
4. 设计客户端恢复
收到 412 后获取当前文档,展示冲突字段或差异。只有字段语义和权限规则允许时才自动合并。重试必须使用新返回的 ETag,并保持资源层面的幂等性;不要盲目重放旧请求。
5. 保持缓存与副本一致
验证器应来自写入路径可见的已提交状态。落后副本可能返回旧 ETag,造成不必要冲突;写请求转到其他节点时,仍必须由主事务执行版本检查。记录冲突率、缺少 If-Match 比率和重试成功率。
高质量示范回答
“我会为每个可编辑文档返回强 ETag,并要求变更请求带 If-Match。服务端在更新行的同一事务中比较版本;没有匹配行就返回 412,且不执行副作用。客户端重新获取文档,展示差异或按明确规则合并,再用新 ETag 重试。我会把 412 与校验、权限错误区分开,并监控旧读、冲突和重试成功率。”
常见误区
- 先检查 ETag 再单独更新 → 仍存在竞态 → 在一个原子版本条件更新中完成。
- 精确写入使用弱验证器 → 比较可能失真 → 使用强验证器。
- 所有过期写入都返回 409 → 客户端无法识别协议前置条件 → If-Match 失败使用 412。
- 盲目重试旧载荷 → 可能覆盖第一次修改 → 重新获取、明确合并,再用新标签重试。
追问与回答
ETag 只能用于缓存吗?
不能。If-None-Match 常用于缓存校验,If-Match 则让写入依赖当前表示。只要比较强度和生成策略正确,同一个验证器可以承担两种用途。
客户端不发送 If-Match 怎么办?
若资源要求乐观并发,应按契约拒绝,并返回明确的前置条件要求或 400 策略。静默接受无条件写入会重新引入丢失更新;各方法必须保持一致。
412 响应应返回最新文档吗?
在权限和载荷大小允许时可以返回,但契约仍应要求重新获取或显式解决冲突。返回数据不代表客户端可以不带最新验证器覆盖它。