四层缓存堵的是四个不同位置的等待。顺序配错,会出现两种典型结果:加了一堆缓存但页面还是慢,或者后台改了内容前台还是旧的。
建议的顺序:
例外:登录用户占比高的站(电商、会员站、论坛)把对象缓存提到页面缓存前面。这类请求基本都会被页面缓存排除掉,先上页面缓存拿不到多少收益。
为什么前两层排在前面:它们不动 PHP、不动数据库,只改响应头和分发路径,配错改回去就行;而且它们决定了后面两层的效果能不能被用户感知到——源站再快,用户隔着半个地球访问一样慢。
层 | 命中时省掉什么 | 收益落在谁身上 | 生效位置 |
|---|---|---|---|
浏览器缓存 | 重复下载同一个 CSS / JS / 图片 | 回访用户、站内跳转 | 用户设备 |
CDN | 用户到源站那段网络往返 | 异地用户 | 边缘节点 |
页面缓存 | PHP 执行 + 查库 + 拼 HTML 整段 | 未登录访客 | 源站 |
对象缓存 | 单次请求里重复的同一份查询 | 未命中页面缓存的请求、登录用户 | 源站内存 |
配错的典型症状:
现象 | 多半是 |
|---|---|
首字节很快,图片慢慢刷出来 | 只做了页面缓存,没做 CDN |
回访用户每次都重新下载 CSS | 没设 Cache-Control,或资源 URL 没有版本参数 |
后台改了价格,前台还显示旧的 | 页面缓存或 CDN 缓存了 HTML,没有失效机制 |
加完购物车页面变成「未登录」版本 | 页面缓存没排除带 cookie 的请求 |
上了对象缓存,TTFB 没变化 | 页面缓存命中率高,请求根本没走到 PHP |
for i in 1 2 3 4 5; do
curl -o /dev/null -s -w 'ttfb:%{time_starttransfer} total:%{time_total}\n' https://example.com/
done跑五次,丢掉第一次(TCP、TLS 握手会拖高),看后四次的 time_starttransfer。后面每做完一层,用同一条命令再跑一次对比。没有这个数,后面所有判断都是感觉。
原理一句话:让浏览器自己把文件存下来,下次访问不再问服务器。前提是文件名或 URL 带版本参数,否则你改了文件,用户看到的还是旧的。
server {
# 加在已有的 server 块里
location ~* \.(css|js|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf)$ {
add_header Cache-Control "public, max-age=2592000" always;
access_log off;
try_files $uri =404;
}
}一个必须知道的坑:add_header 在 location 里会覆盖从上层继承下来的所有 add_header。如果 server 块里还有别的响应头(安全头之类),要在这个 location 里重复写一遍,否则它们会在这个 location 里消失。
WordPress 的 wp_enqueue_style() / wp_enqueue_script() 默认会带 ?ver=,文件改了参数就变,缓存自然失效。主题里手写 <link> 的没有这层保护,改版之后要自己改文件名或者加参数。
验证:
curl -sI https://example.com/wp-content/themes/your-theme/style.css | grep -i cache-control输出里有 max-age=2592000 就对了。
CDN 治的是距离:源站在一个地方,用户在另一个地方,每个请求都得跑一趟。它不管你源站多快,只管少跑路。
先只接静态资源,HTML 的缓存不要现在开。 HTML 里带着价格、库存、登录状态,边缘节点分不清这次请求该给谁,缓存了就是事故。
要设置的三件事:
Cache-Control 走,不要另设一套,否则两边打架,排查起来很痛苦。验证:
curl -sI https://example.com/wp-content/uploads/2026/01/pic.jpg | grep -i age
sleep 3
curl -sI https://example.com/wp-content/uploads/2026/01/pic.jpg | grep -i age第二条的 age 应该比第一条大,说明命中的是边缘节点而不是源站。很多 CDN 还会返回自己命名的命中状态头,看 age 是最通用的办法。
这是收益最大的一层:生成一次 HTML 要跑 PHP、查几十次库,缓存下来这些全省了。用 nginx 的 fastcgi_cache 做,不依赖插件,出问题也好定位。
fastcgi_cache_path /var/cache/nginx/wp levels=1:2 keys_zone=wp_cache:100m max_size=2g inactive=60m use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# 带这些 cookie 的请求不缓存
map $http_cookie $skip_cookie {
default 0;
"~*wordpress_logged_in" 1;
"~*comment_author" 1;
"~*wp-postpass" 1;
"~*woocommerce_items_in_cart" 1;
"~*woocommerce_cart_hash" 1;
"~*wp_woocommerce_session" 1;
}
# 只缓存 GET / HEAD
map $request_method $skip_method {
default 1;
GET 0;
HEAD 0;
}
# 这些路径不缓存
map $request_uri $skip_uri {
default 0;
"~*/wp-admin/" 1;
"~*/wp-login\.php" 1;
"~*/cart/" 1;
"~*/checkout/" 1;
"~*/my-account/" 1;
"~*preview=true" 1;
"~*[?&]s=" 1;
}
# 三个条件拼起来,任何一个为 1 就跳过缓存
map "$skip_cookie$skip_method$skip_uri" $skip_cache {
default 0;
"~1" 1;
}$request_uri 是带查询串的,所以 [?&]s= 能匹配到搜索页。搜索页不缓存是因为参数组合太多,缓存了只会把磁盘塞满。
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_cache wp_cache;
fastcgi_cache_valid 200 301 302 10m;
fastcgi_cache_valid 404 1m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_lock on;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_background_update on;
add_header X-Cache $upstream_cache_status always;
}fastcgi_cache_bypass 和 fastcgi_no_cache 要同时设:前者是不读缓存,后者是不写缓存。只设一个会出现「登录用户看到的内容被写进了公共缓存」。
如果你的 nginx 不认识 fastcgi_cache_background_update,把那一行删掉即可,不影响其他配置。
# Debian/Ubuntu 是 www-data,CentOS/RHEL 是 nginx
sudo mkdir -p /var/cache/nginx/wp
sudo chown www-data:www-data /var/cache/nginx/wp
sudo nginx -t && sudo systemctl reload nginxcurl -sI https://example.com/ | grep -i x-cache # 第一次:MISS
curl -sI https://example.com/ | grep -i x-cache # 第二次:HIT
curl -sI -H 'Cookie: wordpress_logged_in_abc=1' https://example.com/ | grep -i x-cache # 应为 BYPASS第三条是关键。如果带登录 cookie 还能拿到 HIT,说明你的 map 没生效,线上马上就会出现「A 用户看到 B 用户信息」。
sudo find /var/cache/nginx/wp -type f -deletefastcgi_cache_purge 是第三方模块,不是所有 nginx 都编进去了。没装就用上面这条,删文件一样有效。
对象缓存治的是「一次请求里同一份数据被反复查」。页面缓存是把整页 HTML 存下来,对象缓存是把数据库查询结果存到内存里,多个请求共用。
先判断值不值得上。 看单次请求的查询次数:
wp eval 'global $wpdb; echo "queries: ", $wpdb->num_queries, "\n";'同一个页面多跑几次。如果这个数一直是几十上百,说明每次都在重复查同样的东西,对象缓存有收益;如果只有十几次,先别折腾,把时间花在慢查询上更划算。
配好之后必须验证是否真的持久化。 很多人以为配好了,其实还是单请求内存缓存:
# 第一个进程写入
wp eval 'wp_cache_set("probe", "1", "aumtest"); echo "set ok\n";'
# 第二个进程读取
wp eval 'var_dump(wp_cache_get("probe", "aumtest"));'第二条输出 string(1) "1",说明跨请求共享生效了;输出 bool(false),说明还是单进程内存缓存。原理很简单:wp eval 每次都是新进程,进程内存里的东西活不过一次执行。这个验证方法不依赖任何插件。
还有一个容易忽略的点:页面缓存命中时根本不执行 PHP,对象缓存没机会起作用。所以页面缓存命中率越高,对象缓存能优化的那部分就越小。先看命中率,再决定要不要多维护一套 Redis。
内容更新之后,刷新的顺序是固定的:
反过来做会把旧内容推回 CDN:你先刷 CDN,用户请求打回源站,源站还是旧 HTML,CDN 又把这份旧的拉走存起来。看上去「刷了没用」,其实是顺序错了。
全部做完,再跑一次 Step 0 的 curl,和基线对比。如果 TTFB 没降,问题多半不在缓存层:可能是慢查询、外部 API 调用、或者 PHP-FPM 进程数不够。这时候继续往上叠缓存只会让排查更困难。
更多 WordPress 的实操记录在 AumCreate 的博客。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。