"清一下缓存试试"可能是运维和客服说得最多的一句话。但缓存其实分好几种,浏览器缓存和服务器缓存完全是两回事,很多人把它们混着讲,排障时就抓不住重点:明明清了浏览器缓存,问题还在;或者明明是服务器缓存的问题,却让用户反复刷新浏览器。
这篇文章把缓存从"浏览器"到"服务器"这条链路完整拆开讲:HTTP 缓存头是怎么工作的、强缓存和协商缓存有什么区别、Nginx 上的几种缓存、以及应用层的 Redis 缓存,最后讲多级缓存是怎么协同的。看完之后,你应该能判断一个"内容没更新"的问题到底卡在哪一级缓存。
一、缓存存在的意义:把重复的工作省掉
缓存的第一性原理非常简单:同样的结果如果算过一次、传过一次,就存下来下次直接用,别再算、别再传。
一次网页请求里,重复的工作有很多:浏览器反复下载同一个 CSS、CDN 反复回源取同一张图、后端程序反复查同一条数据库记录。每一次重复都消耗时间、带宽和计算资源。缓存就是在链路的各个位置把这些重复结果存下来,让下一次请求就近取到。
理解了这一点,也就理解了为什么缓存会分层:越靠近用户的缓存,取用越快、越省资源,但能覆盖的用户越少;越靠近数据源的缓存,取用相对慢,但能服务更多请求。于是就有了"浏览器 → CDN → 反向代理 → 应用 → 数据库"这样一条缓存链,每一层都在试图用自己存储的结果回答请求,回答不了才往下一层走。
二、浏览器缓存:HTTP 缓存头的世界
浏览器缓存是离用户最近的一层,它完全由响应头控制。服务器返回内容时,顺便告诉浏览器"这个内容你可以存多久、怎么判断它过没过期"。
两大类缓存机制要分清:强缓存和协商缓存。
强缓存是在有效期内直接用本地副本,连服务器都不问。控制强缓存的两个头是 Cache-Control 和 Expires:
Cache-Control: max-age=31536000表示从响应时间起一年内有效;Cache-Control: no-cache表示可以存但不能直接用,每次必须去服务器确认(实际上走协商缓存);Cache-Control: no-store表示完全不许缓存,连存都不存;Expires是一个绝对时间,是老式的写法,兼容性保留。
Cache-Control 的优先级高于 Expires,现代配置应以 Cache-Control 为主。
协商缓存是在强缓存失效后,浏览器带着"凭证"去服务器问:这个内容变了没有?没变就返回 304(Not Modified),浏览器继续用本地副本,省下传输内容的流量。凭证有两种:
ETag/If-None-Match:服务器给内容算一个标识(比如哈希),浏览器下次带上If-None-Match:,服务器比对,没变就返回 304;Last-Modified/If-Modified-Since:服务器给一个最后修改时间,浏览器下次带上If-Modified-Since,服务器比对文件的修改时间。
| 机制 | 控制头 | 行为 | 是否请求服务器 |
|---|---|---|---|
| 强缓存 | Cache-Control / Expires | 有效期内直接用本地副本 | 否 |
| 协商缓存 | ETag / Last-Modified | 失效后去服务器确认 | 是,但可能只返回 304 |
一次完整的浏览器缓存判断顺序大致是:先看有没有强缓存且未过期 → 有就直接用;没有或过期 → 带上协商缓存凭证去服务器 → 服务器判断未变则 304、变了则返回新内容。
# 查看响应头里的缓存字段
curl -I https://www.example.com/static/app.js
# 关注 Cache-Control、Expires、ETag、Last-Modified
# 用 if-none-match 模拟协商缓存,服务器未变会返回 304
etag=$(curl -sI https://www.example.com/static/app.js | awk -F': ' 'tolower($1)=="etag"{print $2}' | tr -d '\r')
curl -I -H "If-None-Match: $etag" https://www.example.com/static/app.js
一个常见误区是"设置 no-cache 就是完全不缓存"。其实不是,no-cache 是"可以缓存,但每次使用前必须去服务器确认",真正不缓存的是 no-store。这个区别在排查"为什么还是发了请求"时很关键。
三、CDN 缓存与反向代理缓存
浏览器之后的一层是 CDN。CDN 缓存有两点和浏览器缓存不同:一是它对所有用户共享(同一个资源只存一份,服务很多人),二是它的缓存有效期可能被 CDN 侧的规则覆盖。
CDN 默认会尊重源站的 Cache-Control,但很多服务商允许在控制台配置覆盖规则,比如强制把某类文件缓存一年。这也意味着:如果 CDN 侧配了强制缓存,源站改了缓存头但没刷新 CDN,用户看到的仍然是旧规则。
# 判断内容是从 CDN 缓存返回还是回源取得的
curl -I https://www.example.com/static/app.js
# 看 X-Cache / Via / Age 等字段
# Age 表示内容在缓存里已经存了多久
再往内是反向代理缓存,最有代表性的是 Nginx 的 proxy_cache。它把后端应用的响应缓存在 Nginx 的磁盘上,减少后端压力。它和浏览器缓存的作用对象不同:浏览器缓存服务"这个用户",Nginx 缓存服务"所有经过它的请求"。
四、服务器端的应用缓存与数据库缓存
前面几层都是缓存"HTTP 响应",而应用层缓存缓存的是"数据和计算结果",最常用的载体是 Redis 和 Memcached。
典型场景:一个首页需要查数据库里几十条商品信息,每次请求都查库会很慢。做法是第一次查完把结果序列化后放进 Redis,之后直接读 Redis,读不到(缓存未命中)再查库并把结果写回 Redis。这就是常说的"旁路缓存"模式。
应用层缓存和 HTTP 缓存的本质区别:HTTP 缓存缓存的是"发给用户的最终结果",应用层缓存缓存的是"业务处理中的中间结果或数据"。前者减少了网络传输和请求数,后者减少了数据库压力和计算量,两者目标是不同的。
数据库自己也有缓存层,比如 InnoDB 的 Buffer Pool、查询缓存等,属于最靠近数据的缓冲,通常不需要人工精细干预,但它是整条链路的最后一道缓存。
五、Nginx 的几种缓存方式对比
在 Nginx 这一层,常见的缓存有好几种,容易混淆,这里做个对比。
| 缓存方式 | 缓存对象 | 典型指令 | 适用场景 |
|---|---|---|---|
| 反向代理缓存 | 后端 HTTP 响应 | proxy_cache 系列 | 代理到后端应用或第三方接口 |
| FastCGI 缓存 | PHP-FPM 的响应 | fastcgi_cache 系列 | PHP 站点,动态页面静态化 |
| uWSGI 缓存 | uWSGI 的响应 | uwsgi_cache 系列 | Python uWSGI 应用 |
| 静态文件缓存 | 直接读文件 | 操作系统 page cache | 静态文件天然受益 |
其中 fastcgi_cache 对 PHP 站点很有价值:它把 PHP 渲染好的页面缓存起来,之后的请求直接由 Nginx 返回缓存的 HTML,连 PHP 都不用执行,速度提升非常明显。
# fastcgi_cache 示例(PHP 站点)
# http 块内:
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=fcgi:100m max_size=5g inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# server / location 块内:
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_cache fcgi;
fastcgi_cache_valid 200 302 10m;
fastcgi_cache_valid 404 1m;
# 带登录态 Cookie 的请求不缓存,避免缓存私人内容
fastcgi_cache_bypass $cookie_logged_in;
fastcgi_no_cache $cookie_logged_in;
add_header X-FastCGI-Cache $upstream_cache_status;
}
注意 fastcgi_cache_bypass 和 fastcgi_no_cache 配合 Cookie 使用,这是防止把登录用户的私人页面缓存给其他人的关键防护。改配置前依旧要备份并 nginx -t 验证。
mkdir -p /var/cache/nginx/fastcgi
chown -R www-data:www-data /var/cache/nginx # RHEL 系为 nginx:nginx
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%F)
nginx -t && systemctl reload nginx
六、多级缓存是怎么协同的
把前面几层串起来,一个优化良好的站点通常是这样的请求路径:
用户浏览器
↓ ①浏览器缓存(强缓存命中→0 请求)
CDN 边缘节点
↓ ②CDN 缓存(命中→不回源)
Nginx 反向代理 / FastCGI 缓存
↓ ③代理层缓存(命中→不打后端)
应用进程内存缓存 / Redis
↓ ④应用缓存(命中→不查库)
数据库 Buffer Pool
↓ ⑤数据库缓冲(命中→不读盘)
磁盘
每一层的命中都会让请求提前返回,不到下一层。设计多级缓存的核心思路是:热数据往上层放,让高并发的请求在靠前的层就被消化掉。 浏览器和 CDN 吸收海量的静态请求,Redis 吸收大量的重复查询,数据库只处理真正必要的数据操作。
但多级缓存也带来一个必须面对的问题:一致性。同一条数据在多个地方都有副本,更新时如果只更了其中一层,就会出现"用户看到旧数据"的现象。这是缓存设计里最麻烦的部分,常用手段有:更新数据时删除缓存(而不是更新缓存)、给缓存设合理的过期时间兜底、对一致性要求高的场景用"延迟双删"或订阅数据库变更来主动失效。
# 在 Nginx 上观察各级缓存的命中情况
grep -o 'X-Cache-Status: [A-Z]*' /var/log/nginx/access.log 2>/dev/null | sort | uniq -c
# 更常见的做法是开启 Nginx 的 $upstream_cache_status 日志后统计
七、排障:内容没更新该查哪一级
遇到"改了内容用户看不到",按从近到远逐层排查:
- 浏览器缓存:用户强刷(Ctrl+F5)或用无痕窗口,还在说明不是浏览器;
- CDN 缓存:看响应头
Age和缓存状态,必要时刷新 CDN 缓存; - Nginx 代理缓存:看
X-Cache-Status,清理对应缓存目录或刷新; - 应用/Redis 缓存:检查 Redis 里对应 key 是不是旧值,删除后重新生成;
- 数据库:确认数据本身是否已经更新成功。
# 逐层验证,从源站直连绕过所有前置缓存
curl -I -H "Host: www.example.com" http://源站IP/path # 直连源站,看真实内容
curl -I "https://www.example.com/path?nocache=$(date +%s)" # 加随机参数,看是否绕过 CDN
常见问题
Q:用户说清缓存也没用,怎么办?
清浏览器缓存只解决第一层。如果问题在 CDN 或服务器缓存,用户怎么清都没用。这时需要运维在服务端刷新缓存或排查一致性逻辑。
Q:Cache-Control 和 Expires 冲突听谁的?
听 Cache-Control。Expires 是为兼容老客户端保留的,现代配置以 Cache-Control 为主,用 expires 指令时 Nginx 会同时设置,一般不会冲突。
Q:no-cache 和 no-store 有什么区别?
no-cache 是"可以存,但每次用之前必须去服务器确认"(走协商缓存,可能返回 304);no-store 是"根本不存",每次都要完整重新获取。
Q:Redis 缓存和 Nginx 缓存重复吗?
不重复,它们缓存的对象不同。Nginx 缓存的是 HTTP 响应(最终结果),Redis 缓存的是应用数据(中间结果)。一个减少网络和渲染开销,一个减少数据库压力。
Q:多级缓存的过期时间怎么设?
一般让越靠上的层过期时间越短,或至少不短于下层,避免出现"上层缓存比下层还新"的混乱。但更稳妥的做法是靠主动失效(更新时删缓存)配合过期时间兜底。
总结
缓存分很多层,浏览器缓存和服务器缓存是其中两端。浏览器缓存由 HTTP 缓存头控制,分强缓存(Cache-Control/Expires,有效期内不问服务器)和协商缓存(ETag/Last-Modified,失效后带凭证去确认,未变更返回 304)。
no-cache 不等于不缓存,它表示每次使用前要确认;真正不缓存是 no-store。这个区别在排障时很重要。
服务器侧有三类缓存:CDN 缓存(共享、可被规则覆盖)、Nginx 缓存(proxy_cache / fastcgi_cache,缓存 HTTP 响应)、应用层缓存(Redis 等,缓存数据和计算结果)。它们缓存的对象不同,不是一回事。
多级缓存按"浏览器→CDN→反向代理→应用→数据库"的顺序协同,每一层命中都会让请求提前返回。设计的关键是把热数据往上层放,同时用主动失效加过期兜底来应对一致性。
遇到内容没更新,按从近到远逐层排查,并用直连源站和加随机参数两种方式快速判断缓存卡在哪一级。理解这套链路,就不会再把"清缓存"当成万能钥匙了。
企业QQ咨询




