概述
ETag 是某个具体表述的不透明校验器。客户端带着 If-None-Match 再次请求时,服务端只需返回 304 Not Modified 与响应头,一次 80 KB 的 JSON 下载就变成几百字节,同时客户端仍能立刻感知内容变化。与设置较长的 max-age 不同,协商缓存不会推迟更新。
步骤
- 明确校验器强度:只有字节完全一致才用强校验器;动态压缩或经过 CDN 改写时应使用弱校验器,
If-None-Match本身也按弱比较处理。 - 基于规范化后的内容生成标签,排除
updatedAt、trace ID 等易变字段,并按主键稳定排序。 - 设置
Cache-Control: no-cache与ETag,用 Express 的req.fresh判断是否返回 304:
app.get('/api/orders', async (req, res) => {
const rows = await listOrdersCanonical();
res.set('Cache-Control', 'no-cache');
res.set('ETag', weakTag(rows));
if (req.fresh) return res.status(304).end();
res.json(rows);
});
- 若用数据库版本列派生标签,必须保证每次写入都递增版本,否则客户端会在合法 304 后面缓存到旧数据。
验证
先请求一次拿到 ETag,再带上 If-None-Match 请求,应得到 304 且 size_download 为 0。检查 304 响应仍然重复 ETag、Cache-Control 与 Vary,且没有响应体。连续请求同一份未变化的数据,重启进程后标签也应保持一致。
注意事项
- 服务端必须先构造完整表述才能计算哈希,因此 ETag 无法降低 CPU 密集型接口的开销。
- 标签只对应一个 URL,查询参数变化必须改变响应内容,从而改变标签。
- 序列化格式变更会让所有标签同时失效,这可以接受,但部署后应观察 304 比例确认缓存正常再验证。
- 304 若错误地带上响应体,会同时浪费带宽并让部分客户端解析失败。