浏览器缓存和服务器缓存有什么区别?缓存机制全面解析

"清一下缓存试试"可能是运维和客服说得最多的一句话。但缓存其实分好几种,浏览器缓存和服务器缓存完全是两回事,很多人把它们混着讲,排障时就抓不住重点:明明清了浏览器缓存,问题还在;或者明明是服务器缓存的问题,却让用户反复刷新浏览器。

这篇文章把缓存从"浏览器"到"服务器"这条链路完整拆开讲: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 日志后统计

七、排障:内容没更新该查哪一级

遇到"改了内容用户看不到",按从近到远逐层排查:

  1. 浏览器缓存:用户强刷(Ctrl+F5)或用无痕窗口,还在说明不是浏览器;
  2. CDN 缓存:看响应头 Age 和缓存状态,必要时刷新 CDN 缓存;
  3. Nginx 代理缓存:看 X-Cache-Status,清理对应缓存目录或刷新;
  4. 应用/Redis 缓存:检查 Redis 里对应 key 是不是旧值,删除后重新生成;
  5. 数据库:确认数据本身是否已经更新成功。
# 逐层验证,从源站直连绕过所有前置缓存
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→反向代理→应用→数据库"的顺序协同,每一层命中都会让请求提前返回。设计的关键是把热数据往上层放,同时用主动失效加过期兜底来应对一致性。

遇到内容没更新,按从近到远逐层排查,并用直连源站和加随机参数两种方式快速判断缓存卡在哪一级。理解这套链路,就不会再把"清缓存"当成万能钥匙了。