ETag — 包含资源版本唯一标识符的 HTTP 响应标头。服务器将 ETag 生成为内容的哈希或版本号,并将其与数据一起返回给客户端。在后续请求中,客户端将此标识符发送到 If-None-Match 标头中,使服务器能够检查资源是否已更改。根据 MDN Web Docs, 2025,ETag 是 HTTP 中条件 GET 请求机制的基础。使用 ETag 的条件请求可将移动应用程序同步时的数据传输量减少高达 90%。
主要要点
ETag(实体标签) — 来自条件标头家族的 HTTP 标头,用于验证缓存的资源。服务器将 ETag 计算为哈希值(MD5、SHA-256)或资源的版本号,并在对 GET 请求的响应中返回。客户端将 ETag 与数据一起保存,并在后续请求中将其发送到 If-None-Match 标头中。如果资源内容未更改,服务器会以 304 Not Modified 状态响应,不包含响应体。
对于移动应用程序,ETag 至关重要,因为它减少了下载的数据量。在每次启动或同步时,应用程序通过 If-None-Match 请求检查资源的最新状态——而不是下载完整数据,它收到 304 并使用本地副本。根据 Google Chrome Team (2024),在移动 API 中使用 ETag 可使列表的平均响应量减少 87%,单个对象减少 94%。
ETag 在服务器端生成,既可以确定性的(相同内容生成相同 ETag,适用于共享缓存),也可以对每个响应唯一(用于严格验证)。在用于移动同步的 REST API 中,最常用的是内容哈希和数据库中记录版本号的组合。
强 ETag(strong ETag) — 随任何内容更改(包括微小更改,如空格、格式)而变化的标识符。格式:"abc123def"(在双引号内,无前缀)。强 ETag 保证资源未逐字节更改。它们对于范围请求(Range requests)和检查部分下载的完整性是必需的。
弱 ETag(weak ETag) — 带有前缀 W/ 的标识符,例如 W/"abc123def"。它们允许资源在语义上等价,即使字节表示不同。弱 ETag 对于动态生成具有不同空格或格式但含义相同的响应的服务器非常有用。但是,弱 ETag 不支持范围请求。
ETag 类型比较:
| 特性 | 强 ETag | 弱 ETag |
|---|---|---|
| 格式 | "hash" | W/"hash" |
| 敏感度 | 逐字节 | 语义 |
| 范围请求 | 支持 | 不支持 |
| CDN 缓存 | 理想 | 有限 |
| 同步 | 高精度 | 允许冲突 |
Last-Modified — 指示资源最后修改日期和时间的 HTTP 标头。客户端在 If-Modified-Since 标头中将其发送回。Last-Modified 实现更简单(服务器只需要日期),但有根本性的限制:一秒的分辨率(一秒内的两次更改无法区分)以及无法确定内容在相同时间是否已更改(例如,从备份恢复后)。
ETag 解决了这些问题:内容的哈希值会随每次更改而变化,与时间无关。因此,现代 REST API 使用两者的组合:ETag 用于精确验证,Last-Modified 用于在 CDN 上进行近似过滤。Apache HTTP Server 和 Nginx 默认会为静态文件生成这两个标头。
对于需要同步的移动应用程序,ETag 更为关键,因为它可以检测编辑冲突。如果客户端发送带有 If-Match: "etag" 标头的 PUT 请求,当资源已被其他客户端更改时,服务器会拒绝该请求(乐观锁定)。由于秒级精度,Last-Modified 无法保证这种可靠性。
我们来看一下移动应用程序中 ETag 的客户端实现,使用 Kotlin 和 Retrofit 及 OkHttp。在每个 GET 请求中,客户端从响应中保存 ETag,并在下一个请求中将其发送到 If-None-Match 标头中。如果服务器返回 304,数据不会重新下载。
配置带有 ETag 缓存的 OkHttp 客户端:
class EtagClient {
private val etagCache =
mutableMapOf<String, String>()
private val client = OkHttpClient.Builder().build()
suspend fun fetchWithEtag(
url: String
): Result<String> {
val request = Request.Builder()
.url(url)
.header("If-None-Match",
etagCache[url] ?: "")
.build()
val response = client.newCall(request).await()
return when (response.code) {
304 -> Result.success(
"not_modified")
200 -> {
response.header("ETag")?.let {
etagCache[url] = it
}
Result.success(response.body?.string()
?: "")
}
else -> Result.failure(
Exception("HTTP ${response.code}"))
}
}
}
客户端在成功 200 响应后保存 ETag,并在下一个请求中将其发送到 If-None-Match 标头中。收到 304 时,客户端知道本地版本是最新的,不会浪费流量重新下载数据。这种模式可将移动应用程序的网络成本降低 80–90%(对于频繁请求的资源)。
ETag 是优化移动应用程序与 REST API 同步的关键机制。在标准同步方案中,客户端首先请求带有 ETag 验证的资源列表——如果没有资源发生变化,服务器返回 304,客户端结束同步。如果有更改,服务器只返回修改过的资源。这种方法称为增量同步,对于流量受限的移动设备至关重要。
在乐观锁定场景中,ETag 用于防止 Lost Update 冲突。当客户端发送 PUT 请求更新资源时,它会包含 If-Match: "etag" 标头。如果 ETag 不匹配(其他客户端已更改资源),服务器会以 412 Precondition Failed 响应,客户端必须重新下载最新版本并重试更改。这种方法在无需数据库级别锁定的情况下确保了数据一致性。
对于具有离线模式的分布式系统,ETag 与冲突解决结合使用。客户端通过获取所有资源的当前 ETag 进行同步。发送更改时,服务器检查 If-Match——如果 ETag 不匹配,则会记录一个冲突,该冲突根据所选策略(LWW、Merge)解决。根据 Postman API Report (2025),67% 的用于移动应用程序的生产 REST API 使用 ETag 作为主要版本验证机制。
常见问题
ETag — 包含资源版本唯一标识符的 HTTP 响应标头。客户端将其用于条件请求:如果资源未更改,服务器返回 304 Not Modified(无响应体),从而节省流量。
ETag 使用内容哈希进行精确比较。Last-Modified 基于修改日期,精确到秒。ETag 在检测实际更改方面更可靠,并通过 If-Match 支持乐观锁定。
强 ETag(无前缀)逐字节区分资源。弱 ETag(带前缀 W/)允许语义等价。强 ETag 对范围请求是必需的,弱 ETag 适用于动态生成的内容。
ETag 可减少 80–90% 的流量:客户端通过 If-None-Match 检查所有资源的最新状态,只下载已更改的资源。如果没有 ETag,客户端在每次同步时都会下载完整数据,浪费流量和电池。
服务器计算 ETag 为响应内容的哈希值(MD5、SHA-256)或使用数据库中的记录版本号。在 Spring Boot 中,使用 @Cacheable 注解并设置 etag = true 即可。在 Express.js 中,etag 中间件默认启用。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。