ETag:是什么、缓存机制以及头的配置

作者: IT Sectr 发布日期: 2026-03-09 阅读时间: 8 分钟

ETag (Entity Tag) — HTTP头部,用于在服务器上分配资源版本的唯一标识符,允许客户端高效地检查缓存数据的最新状态。在重复请求时,浏览器或应用程序发送已保存的ETag,服务器将其与当前值进行比较:若匹配,则返回304 Not Modified状态,无响应主体。根据RFC 7232 (IETF, 2014),使用ETag的条件请求可将常请求的资源传输数据量减少高达95%。这使得该头部对移动应用的性能至关重要。

主要要点

  • ETag — HTTP头部,带有资源版本的唯一标识符,用于条件请求和缓存
  • 工作原理 — 服务器生成内容散列或版本号,客户端在If-None-Match头中发送
  • 强ETag和弱ETag — 强ETag(内容字节完全相同)和弱ETag(内容语义等价,前缀W/uff09
  • 304 Not Modified — ETag匹配时服务器的响应,节省带宽并加快加载
  • ETag与Last-Modified对比 — ETag更精确(内容散列),Last-Modified更简单(日期),结合使用可实现最大效率

什么是ETag?

ETag (Entity Tag) — 是一个HTTP响应头部,包含特定资源版本的唯一标识符。服务器根据文件内容、元数据或修订号计算ETag,并在对GET请求的响应中传递给客户端。客户端保存这个标识符,并在随后对同一资源的请求中将其发送在If-None-Match头中。如果资源未变更,服务器以304 Not Modified响应,客户端使用其缓存副本。

ETag的格式在RFC 7232中被定义为引号内的字符串:"33a64df551425fcc55e4d42a148795d9f25f89d4"。值可以是文件内容的SHA-1散列、递增版本号、静态文件的inode-编号-时间组合或服务器生成的任意标记。唯一的要求是值必须随资源的每次变更而变化,并在资源保持不变时保持不变。

ETag属于条件请求(conditional requests)机制 — HTTP协议的基础优化之一。与无条件请求(服务器始终返回完整响应)不同,条件请求允许客户端检查缓存的最新状态而无需重新下载数据。根据HTTP Archive (2025)的数据,由于ETag和Last-Modified的正确配置,约40%的所有HTTP响应都是304 Not Modified。

ETag的应用场景

ETag在REST API中用于优化数据集合的加载 — 如果对象列表未变更,客户端获得304而无需发送整个JSON。在静态文件(CSS、JS、图片)中,ETag允许CDN和浏览器高效检查缓存的最新状态。在移动应用中,ETag对于后台同步至关重要:应用检查服务器上的数据是否已变更,并仅在必要时下载更新。这节省了带宽和设备电池。

ETag如何工作?

ETag的完整工作循环由四个步骤组成。服务器在第一次请求时生成ETag并将其返回在响应头中。客户端将ETag与缓存的资源一起保存。在重复请求时,客户端发送If-None-Match头,带有已保存的ETag值。服务器将接收到的值与资源当前的ETag进行比较:若匹配,返回带有空主体的304 Not Modified;若不匹配,则返回200 OK,带有新资源和新ETag。

http
// 客户端带If-None-Match的请求
GET /api/users HTTP/1.1
Host: example.com
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

// 服务器响应 — 资源未变更
HTTP/1.1 304 Not Modified
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

在移动应用中,这个循环可以通过支持缓存的HTTP客户端实现。例如,OkHttp通过CacheInterceptor自动管理ETag:保存响应的ETag,并在重复请求时添加If-None-Match。接收到304后,OkHttp返回缓存的数据。OkHttp支持ETag无需额外配置 — 只需通过OkHttpClient.Builder.cache()启用缓存即可。

在服务器上生成ETag

服务器可以通过多种方式计算ETag:通过内容的MD5或SHA散列,通过数据库的修订号(例如MySQL的updated_at),通过静态文件的inode + mtime + size组合(Nginx正是这样生成ETag)。对于动态API,最可靠的是内容散列:如果JSON响应改变了任何字段,ETag将会改变。但每次请求都计算散列会增加CPU负荷 — 对于高负荷系统,更好的方法是使用递增版本号。

强ETag和弱ETag

RFC 7232定义了两种ETag类型:强(strong)和弱(weak)。强ETag意味着资源的两种表示字节完全相同 — 没有任何一位不同。弱ETag(前缀W/)仅保证语义等价:内容在序列化层面上可能不同(空格、JSON字段顺序),但数据对客户端而言被视为相同。弱ETag以前缀W/标记,例如W/"1a2b3c"

ETag类型的选择取决于比较精度的要求。对于静态文件(CSS、JS、图片),建议使用强ETag — 如果文件已变更,客户端应获取新版本。对于动态API,其中同一JSON可能以不同的字段顺序或格式进行序列化,弱ETag提供了更大的灵活性:服务器根据业务数据而非字符串表示来生成ETag。

ETag类型格式保证应用
Strong(强)"散列"字节完全相同静态文件、二进制资源
Weak(弱)W/"散列"语义等价JSON API、动态页面

弱ETag的局限性:它们不能与范围请求(Range requests)一起使用。如果客户端请求文件的一部分,服务器必须返回强ETag以保证片段与完整资源相符。弱ETag不提供这种保证。在其他场景中,弱ETag是安全的,并且建议用于API。

ETag与Last-Modified对比

ETag和Last-Modified是用于条件请求的两个HTTP头部,常常一起使用。Last-Modified指示资源的最后一次修改日期,与If-Modified-Since头部配合使用。ETag提供唯一的版本标识符,与If-None-Match配合使用。每种方式都有其优势和局限,组合使用可实现最大缓存效率。

Last-Modified实现起来更简单 — 服务器自动从文件系统获取日期,或更新数据库中的updated_at字段。但日期精度为秒级,这对于每秒变更多次的资源是不够的。此外,Last-Modified无法区分不同的状态:如果文件被相同版本覆盖,日期变了,但内容未变 — 客户端会重新加载相同的数据。

ETag更精确:仅在内容实际变更时改变。如果服务器从备份中恢复了之前的版本,ETag将会改变。如果文件被相同数据覆盖 — ETag保持不变,客户端不会重新加载。HTTP规范建议联合使用:服务器返回两个头部,客户端同时发送If-None-Match和If-Modified-Since。如果至少一个头部指示变更 — 服务器返回新资源。

头部的优先级

根据规范,ETag的优先级高于Last-Modified。如果服务器收到If-None-Match,应仅检查ETag,忽略If-Modified-Since。这防止了竞态条件(race condition):如果资源在客户端发送Last-Modified与服务器检查之间发生了变更,ETag将是更新的指示器。在实践中,服务器通常检查两个头部,但在结果不一致时,ETag获胜。

在服务器上实现ETag

ETag的配置取决于服务器类型。Nginx根据inode、mtime和大小自动为静态文件生成ETag。Apache使用FileETag机制。对于Node.js、PHP、Python、Ruby中的动态应用,ETag需要通过编程方式生成 — 通过响应散列、数据版本号或请求参数组合。

go
func etagMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter,
        r *http.Request) {
        // 根据数据生成ETag
        etag := generateETag(r.URL.Path)
        w.Header().Set("ETag", etag)

        // 检查If-None-Match
        if r.Header.Get("If-None-Match") == etag {
            w.WriteHeader(http.StatusNotModified)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Go中的中间件拦截请求,为所请求的URL生成ETag(例如,计算缓存或数据库中数据的散列),并设置响应头。如果客户端发送了If-None-Match且与当前ETag匹配,服务器立即返回304 Not Modified,无需调用主处理程序。在生产环境中,应添加已计算ETag按URL和参数的缓存,以减轻服务器负荷。

问题和陷阱

在多服务器配置中(round-robin或anycast),同一资源在所有节点上的ETag必须相同。如果ETag是基于文件的inode生成的,而网站在多个服务器上运行,值将会不同。解决方案 — 使用内容散列或集中式版本存储(Redis、etcd)。第二个问题 — gzip压缩:启用压缩时Nginx会改变ETag,这可能导致过多的304。需要配置gzip_vary on以将ETag与压缩内容同步。

常见问题

ETag可以对不同资源相同吗?

可以,如果服务器未明确禁止。ETag不必全局唯一 — 它在特定URL范围内唯一。对于静态文件,使用SHA散列时冲突几率很低,但对于自建生成器可能产生重复。

是否需要为每个资源配置ETag?

ETag对于多次请求且少变更的资源最有效:静态文件、API列表、配置文件。对于只加载一次的唯一页面(例如订单确认页),ETag没有优势。

ETag如何与CDN工作?

CDN在origin请求中考虑ETag以检查缓存的最新状态。如果origin上资源的ETag已变更,CDN下载新版本。Cloudflare和Fastly将ETag作为在origin层面无效缓存的标准机制进行支持。

ETag可以超过255个字符吗?

RFC 7232没有限制ETag的长度,但服务器和代理可能截断或忽略过长的值。建议使用长度为20–40个字符的散列,或版本标识符与校验和的组合。

选择ETag还是Cache-Control?

这两者并非互斥机制。Cache-Control确定缓存策略(存储多久、允许谁使用),而ETag是对缓存资源的验证机制。最优配置包含两个头部的联合使用。

总结

  • ETag — HTTP头部,包含资源版本的唯一标识符,用于条件请求和高效缓存
  • 原理 — 客户端发送带有已保存ETag的If-None-Match,服务器匹配时返回304
  • 强ETag — 对于静态文件字节完全相同,弱ETag — 对于API语义等价
  • ETag比Last-Modified更精确 — 追踪内容而非日期,仅在实际变更时改变
  • 与Last-Modified联合使用 可实现最大缓存效率
  • 服务器端 — 通过内容散列、数据版本号或参数组合生成
  • 建议 — 在移动应用中为所有API端点和静态资源使用ETag

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读