http缓存需同时配置cache-control、etag和last-modified三者:cache-control控制时效与范围,etag推荐用内容哈希确保可靠性,last-modified作备选但精度低;缺一将导致无法条件请求而浪费带宽。

HTTP 响应头里该设哪些缓存字段
服务器端控制文件缓存,核心就看三个响应头:Cache-Control、ETag 和 Last-Modified。缺一个都可能让浏览器反复下载同一文件。
常见错误是只写 Cache-Control: public, max-age=3600,但没配 ETag 或 Last-Modified —— 这样用户点“刷新”或网络切换后,浏览器没法发条件请求(If-None-Match / If-Modified-Since),直接走完整响应,浪费带宽。
-
Cache-Control决定缓存时长和范围:静态资源用public, max-age=31536000(1年),带版本号的文件可放心设长;动态导出文件建议用no-cache(允许缓存,但每次校验) -
ETag推荐用文件内容哈希(如 SHA256),比时间戳更可靠;避免用文件修改时间,因为部署时文件时间可能重置 -
Last-Modified可作为备选,但精度只有秒级,且无法区分内容未变但文件被重写的情况
ASP.NET Core 中如何给 FileStreamResult 加缓存头
默认 FileStreamResult 不带任何缓存头,必须手动注入。别在 Controller 里硬拼响应头,要用 Response.GetTypedHeaders() 接口,否则 ETag 格式容易出错。
典型场景:用户点击下载 Excel 报表,报表内容稳定、生成开销大,希望 1 小时内重复请求走 304。
- 先计算文件内容哈希(比如
SHA256.HashData(stream)),转成 base64 字符串作为ETag - 调用
Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromHours(1) } - 再设
Response.GetTypedHeaders().ETag = new EntityTagHeaderValue($"\"{hash}\"")(注意引号要手动加) - 如果不用
ETag,改用Last-Modified:确保传入的是DateTimeOffset,且 UTC 时间,否则跨时区会失效
C# 客户端(HttpClient)怎么复用缓存响应
HttpClient 默认完全不处理 HTTP 缓存,它不是浏览器,不会自动读取 Cache-Control 或发条件请求。想让它“懂缓存”,得自己套一层逻辑,或者换用 WebView2 / WebClient(不推荐)。
真实需求往往是:桌面工具频繁查同一个 PDF 下载地址,希望本地有就跳过网络请求。
- 最稳的方式是自己实现本地磁盘缓存:用文件名哈希 +
ETag做 key,把响应体和响应头(尤其是Date、Cache-Control)一起序列化存下来 - 检查是否过期时,别只看
max-age,还要算上Age头(如果服务器返回了) - 不要依赖
HttpClient.DefaultRequestHeaders.IfNoneMatch自动管理:它不会自动读本地缓存,也不会自动解析 304 响应体为空的问题 - 若强行用
HttpBaseProtocolFilter(UWP 场景),注意它只支持private缓存,且 .NET 5+ 的HttpClientHandler已移除内置缓存支持
缓存策略和文件变更怎么保持一致
最大的坑不是代码不会写,而是缓存策略和业务语义脱节。比如导出订单报表,用户导出后又补录了一条数据,但缓存还剩 59 分钟,这时候前端看到的仍是旧文件。
解决思路不是延长缓存,而是让缓存“可失效”。关键点在于:URL 是否携带语义信息。
- 优先用带版本或时间戳的 URL:比如
/export/invoice/20240520-123456.xlsx,这样天然绕过缓存,也省去 ETag 计算开销 - 如果必须用固定路径(如
/export/latest.xlsx),那就必须强制校验:服务端每次返回新ETag,哪怕内容没变也要变(比如加随机后缀),但代价是失去缓存收益 - 禁止对用户上传后生成的文件(如图片缩略图)设置永久缓存:一旦原图更新,缩略图 URL 不变,缓存就永远错下去
缓存不是开关,是契约。你告诉浏览器“这个文件一小时内不变”,就得真的一小时内不变,或者有能力在变的时候让它立刻感知到——否则不如不缓。










