HTTP 缓存头 ETag 与 Last-Modified 协作
解读
在国内高并发、高可用业务场景(如电商秒杀、内容平台)中,面试官问“ETag 与 Last-Modified 如何协作”并不是想听背诵定义,而是考察:
- 能否把两个头放在一次完整请求-响应链里讲清楚;
- 能否指出二者优先级、差异点以及 PHP 侧如何生成与校验;
- 能否结合 Nginx / CDN / 浏览器真实行为,说明在 304 回源、缓存穿透、负载均衡下的风险与优化。
一句话:让候选人证明“我能用 PHP 少跑 SQL、让 CDN 少回源、让用户秒开,还不踩坑”。
知识点
-
语义与格式
- Last-Modified:GMT 格式时间,精度秒级,如
Wed, 21 Oct 2015 07:28:00 GMT。 - ETag:实体标签,强校验器,常见
"{inode-size-mtime}"或"{version-hash}",双引号包裹。
- Last-Modified:GMT 格式时间,精度秒级,如
-
协作流程(RFC 9110)
- 客户端首次请求无缓存头,服务端返回 200 并携带
Last-Modified+ETag。 - 后续刷新:
- 若浏览器只存
Last-Modified,则发If-Modified-Since; - 若同时存
ETag,则额外发If-None-Match。
- 若浏览器只存
- 服务端校验顺序:先
If-None-Match,存在且匹配 → 304;否则再看If-Modified-Since。 - 任一条件失败即返回 200 带新实体与新的
ETag/Last-Modified。
- 客户端首次请求无缓存头,服务端返回 200 并携带
-
强弱校验器
- ETag 是强校验器,字节级一致才命中;
- Last-Modified 是弱校验器,1 秒粒度,可能误命中。
-
PHP 生产环境生成策略
- 静态文件:用
filemtime()生成Last-Modified,用md5_file()或crc32b生成ETag; - 动态接口:把“版本号 + 用户ID + 更新时间”拼成哈希,避免 inode 泄露;
- 框架层:Laravel
->etag()、->lastModified()自动设置,Symfony HttpCache 提供Response::setETag()。
- 静态文件:用
-
坑点
- 分布式节点 inode 不同导致 ETag 不一致,需关闭 Nginx
file_etag改用应用层生成; - 毫秒级更新场景,Last-Modified 精度不足,必须依赖 ETag;
- 反向代理默认剔除
ETag仅留Last-Modified,需要显式gzip_static off; etag on;; - 微信、QQ 内置浏览器对 304 支持不完整,需双保险或短缓存 + ETag。
- 分布式节点 inode 不同导致 ETag 不一致,需关闭 Nginx
答案
“我一般在 PHP 里分三步实现二者协作,确保 CDN 回源率 <1%、数据库 QPS 降 80%。
第一步:响应出口统一封装
function sendCacheResponse($data, DateTime $lastModified, string $etagSeed): void
{
$etag = '"' . hash('crc32b', $etagSeed) . '"';
$lastModStr = $lastModified->format('D, d M Y H:i:s') . ' GMT';
header('Cache-Control: public, max-age=0, s-maxage=60');
header("Last-Modified: $lastModStr");
header("ETag: $etag");
// 校验阶段
if (@$_SERVER['HTTP_IF_NONE_MATCH'] === $etag) {
http_response_code(304);
exit;
}
$ifModifiedSince = @$_SERVER['HTTP_IF_MODIFIED_SINCE'];
if ($ifModifiedSince && strtotime($ifModifiedSince) >= $lastModified->getTimestamp()) {
http_response_code(304);
exit;
}
// 返回新内容
echo json_encode($data, JSON_UNESCAPED_UNICODE);
}
第二步:数据源统一版本号
把“表更新时间 + 用户维度版本”放 Redis,毫秒级变更即重新生成 ETag,避免 Last-Modified 1 秒误差带来的脏缓存。
第三步:运维层对齐
Nginx 配置关闭默认 file_etag,统一走应用层;CDN 侧打开“忽略客户端 Last-Modified 仅认 ETag”开关,防止多节点时钟不一致造成 200 误发。
上线后效果:商品详情页 99% 流量 304 回包,首字节时间从 180 ms 降到 25 ms,大促期间带宽节省 35%。”
拓展思考
-
如果业务允许 1 秒内重复提交,ETag 是否还需要?
答:需要。Last-Modified 只能到秒,无法区分同一秒内的两次变更;此时用 ETag 做“秒级内版本”是唯一办法。 -
微服务场景下,BFF(Node)聚合 PHP 接口,如何透传 ETag?
答:BFF 把上游 PHP 返回的ETag/Last-Modified原样带回去,并在自身层再用SURROGATE-KEY做标签缓存,实现多级缓存一致性。 -
当资源同时存在
Range请求(视频拖拽)时,ETag 需要加-range后缀吗?
答:必须。RFC 规定强校验器在 206 响应中需体现范围,否则同一 ETag 会被缓存节点误用于完整文件,导致花屏。 -
PHP 8 的
hash()使用xxh128算法生成 ETag,性能提升 2 倍,但 CDN 仅支持 ASCII 双引号内 128 字节,如何取舍?
答:折中做法:base64_encode(substr(xxh128,0,16)),既压缩长度又保留冲突率 2^-64,已在国内头部短视频平台验证。