答案:HTTP条件请求与范围请求
为什么 ETag 比 Last-Modified 更可靠?如果服务器在 1 秒内多次修改文件,Last-Modified 会出什么问题?
答案:
ETag 本质上是为资源内容生成的"指纹"(通常是哈希值),而 Last-Modified 只是文件修改的时间戳。打个比方:翼王让图妹管理飞翔公司的静态资源服务器,如果一份 api-doc-v2.pdf 文件在 1 秒内被星宇连续修改了 3 次(比如先改标题、再改内容、最后修正错别字),Last-Modified 只能精确到秒级,3 次修改的时间戳完全相同,服务器和客户端都无法区分这 3 个不同版本。
更隐蔽的问题是:某些文件修改后内容可能完全一致(比如只是 touch 了一下文件,或者重新打包但哈希没变),Last-Modified 会变化,导致缓存失效;而 ETag 基于内容计算,内容没变 ETag 就不变,缓存可以继续使用。
在飞翔公司的 CDN 配置中,凌叔就遇到过这个问题——feixiang-app.js 的 Last-Modified 每次构建都会更新,但实际代码没变化,导致全国节点频繁回源。后来改用 ETag(基于文件 MD5),缓存命中率提升了 40%。
# 模拟 ETag 生成(Linux 环境)
etag=$(md5sum api-doc.pdf | awk '{print $1}')
echo "ETag: \"$etag\""
# 输出类似:ETag: "a3f5c8d2e1b..."
假设图妹和星宇同时编辑同一个文档,如何用 If-Match 实现"乐观锁"防止覆盖?画出请求时序图。
答案:
飞翔公司的技术文档库使用乐观锁防止多人协作时互相覆盖。核心思路是:每个文档版本对应一个唯一的 ETag,客户端更新时必须携带 If-Match: <etag>,服务器只有匹配成功才允许写入。
请求时序图:
图妹 服务器 星宇
| | |
|--- GET /doc/design.md ->| |
| (获取版本,ETag: "v1")| |
|<-- 200 + ETag: "v1" ---| |
| | |
| |<-- GET /doc/design.md -|
| | (获取版本,ETag: "v1")|
| |-- 200 + ETag: "v1" --->|
| | |
| | | 星宇先完成编辑
| |<-- PUT + If-Match: "v1" |
| | (内容:星宇的修改) |
| |-- 200 + ETag: "v2" ---->|
| | (更新成功,版本变为v2)|
| | |
| 图妹后完成编辑 | |
|--- PUT + If-Match: "v1" ->| |
| (内容:图妹的修改) | |
| 但此时服务器版本已是v2 | |
|<-- 412 Precondition Failed| |
| | |
| 图妹收到412,重新GET获取v2| |
| 合并自己的修改后再提交 | |
代码示例(Node.js 模拟):
// 服务器端处理逻辑
app.put('/doc/:name', (req, res) => {
const clientETag = req.headers['if-match'];
const currentETag = getDocETag(req.params.name); // 当前版本
if (clientETag !== currentETag) {
// 版本冲突!返回 412
return res.status(412).json({
error: "文档已被他人修改,请重新获取最新版本",
currentETag: currentETag
});
}
// 版本匹配,允许更新
saveDoc(req.params.name, req.body);
const newETag = generateNewETag();
res.setHeader('ETag', newETag);
res.status(200).send("更新成功");
});
这种机制在飞翔公司的 Wiki 系统中每天都在运行,雁姐(产品经理)和波比(设计师)同时编辑需求文档时,系统会友好提示"页面已过期",而不是默默覆盖。
断点续传时,如果服务器不支持 Range 请求,会返回什么状态码?客户端应该如何降级处理?
答案:
如果服务器不支持 Range 请求,它会直接忽略 Range 请求头,返回标准的 200 OK 和完整的响应体。注意不是返回错误状态码——因为 HTTP 规范允许服务器忽略它不认识的请求头,这是"优雅降级"的设计哲学。
在飞翔公司的文件下载中心,风速(运维工程师)遇到过这种情况:某个旧版 Nginx 配置遗漏了 add_header Accept-Ranges bytes,客户端请求断点续传时,服务器直接返回 200 和整个文件。
客户端降级策略:
- 检测响应状态码:收到 200 而非 206,说明服务器不支持 Range
- 检测响应头:看是否有
Accept-Ranges: bytes,如果没有则标记该资源不支持断点续传 - 重新下载:丢弃已下载的部分(或根据 Content-Length 判断是否兼容),从头开始全量下载
# 客户端用 curl 测试服务器是否支持 Range
curl -I -H "Range: bytes=0-99" http://cdn.feixiang.net/video/intro.mp4
# 支持 Range 的响应:
# HTTP/1.1 206 Partial Content
# Accept-Ranges: bytes
# Content-Range: bytes 0-99/1024000
# 不支持 Range 的响应:
# HTTP/1.1 200 OK
# (没有 Accept-Ranges 头,返回完整内容)
# Python 客户端降级逻辑示例
import requests
url = "http://cdn.feixiang.net/large-file.zip"
headers = {"Range": "bytes=102400-204800"}
resp = requests.get(url, headers=headers, stream=True)
if resp.status_code == 206:
# 断点续传成功,追加写入文件
with open("file.zip", "ab") as f:
f.write(resp.content)
elif resp.status_code == 200:
# 服务器不支持 Range,降级为全量下载
print("警告:服务器不支持断点续传,重新全量下载...")
with open("file.zip", "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
靓晴(前端负责人)在飞翔网盘项目中就实现了这套降级逻辑:先尝试 Range 请求,如果收到 200,就自动切换为普通下载,同时给用户一个提示"该资源不支持断点续传"。