搜索全站
性能优化 14 分钟阅读

缓存插件与动态内容冲突:排除规则、ESI 与按用户缓存

页面缓存一开,购物车、会员价、登录态就串号?本文讲清 WordPress 缓存插件与动态内容冲突的成因,给出 WP Rocket 排除规则、LiteSpeed Cache ESI 私有缓存、Nginx cookie 排除三段配置,附带上线前回归验证脚本。

U
UFO_PF 2017-12-22 · 阅读 14 分钟
缓存 性能优化
阅读提示
本文约 5,549 字,预计阅读 14 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:购物车里出现了陌生人的名字

一位做外贸独立站的朋友发来截图:WooCommerce 站点开启页面缓存后,首页顶部的「你好,Anna」问候语出现在了所有访客的浏览器里,购物车角标上还挂着别人添加的 3 件商品。更麻烦的是这段错误的 HTML 同时被 CDN 边缘节点缓存,等于把某位真实用户的会话状态公开展示了几个小时,直到有人手动清了缓存才恢复。

这类事故的根源不是插件有 bug,而是页面缓存的模型和动态内容的模型天然对立。页面缓存的目标是「同一个 URL,全人类共用一份 HTML」,速度就来自这份共用;而登录态、购物车、会员价、库存、个性化推荐这些内容的本质是「同 URL 不同人结果不同」。只要有一个动态片段被混进共享缓存,就会产生串号。

所以解决问题的思路不是「关掉缓存」,而是把站点切分成静态片段与动态片段两部分,静态部分照常共享缓存,动态部分走排除、ESI 或私有缓存。下面按这个思路逐层展开。

原理分析:缓存到底缓存了什么

页面缓存插件(WP Rocket、LiteSpeed Cache、W3 Total Cache、WP Super Cache)的工作位置在 template_redirect 之后、实际渲染之前:命中缓存时直接输出此前保存好的 HTML 文件,整个主题、数据库查询、大部分插件逻辑都不执行。

内容类型 典型例子 是否可共享缓存 推荐方案
完全静态 文章正文、关于我们、列表页 可以 共享页面缓存,TTL 尽量长
半动态 侧栏最新文章、热门排行 可以,容忍延迟 缓存 + 定时刷新或 AJAX 补
按用户 会员价、已购内容、账户入口 不可共享 私有缓存 / ESI / AJAX
强一致 库存、购物车、结账、支付回调 完全不可 直接排除缓存

三个必须理解的概念:

排除(Exclude / Reject):命中条件的请求压根不查缓存、不写缓存,每次都真实渲染。最安全,也最慢,适用于购物车、结账、我的账户这类页面。

ESI(Edge Side Includes):HTML 里留一个占位标签,边缘服务器在回源时把占位替换成动态片段的真实内容。共享页面缓存照常命中,只有那一个小片段单独请求一次。LiteSpeed、Varnish、Cloudflare 的部分方案都支持。

按用户缓存(Private Cache):仍然缓存,但缓存键里加入用户标识(会话 ID 或用户 ID),每个登录用户一份副本。它比不缓存快,但缓存命中率随用户数下降,磁盘占用会涨,必须给私有缓存设一个较短的 TTL。

保姆级实操

第一步:先把站点的动态片段列成清单

拿一张纸,把所有「因人而异」的输出点列出来:顶部登录条、会员价、购物车角标、心愿单、已购标记、个性化推荐、A/B 测试分组、Cookie 同意横幅。清单之外的内容默认都当静态处理。这一步做不扎实,后面所有规则都会漏项。

第二步:给 WP Rocket 写排除规则

WP Rocket 对 WooCommerce、Easy Digital Downloads 有内置兼容,会自动排除 /cart/、/checkout/、/my-account/ 并识别 woocommerce_items_in_cart 等 Cookie。但自研的会员系统、自定义的动态模板它不认识,需要手动补充:

<?php
// 子主题 functions.php 或 Code Snippets

// 1) 按 URI 片段排除:命中任意一个即不生成缓存
add_filter( 'rocket_cache_reject_uri', function ( $uris ) {
    $uris[] = '/cart/';
    $uris[] = '/checkout/';
    $uris[] = '/my-account/';
    $uris[] = '/members-area/';   // 自研会员专区,插件不认识
    $uris[] = '/api/live-price/'; // 实时报价接口
    return $uris;
} );

// 2) 按 Cookie 排除:请求带上这些 Cookie 就走 BYPASS
add_filter( 'rocket_cache_reject_cookies', function ( $cookies ) {
    $cookies[] = 'edd_items_in_cart';
    $cookies[] = 'my_membership_session';
    return $cookies;
} );

// 3) 只因来源不同、内容完全相同的参数,各自缓存一份即可,不必全排除
add_filter( 'rocket_cache_query_strings', function ( $qs ) {
    $qs[] = 'utm_source';
    $qs[] = 'utm_campaign';
    return $qs;
} );

// 4) 极端场景:某个模板永远不缓存
add_action( 'template_redirect', function () {
    if ( is_page_template( 'template-live-price.php' ) ) {
        define( 'DONOTCACHEPAGE', true );
    }
} );

第三步:LiteSpeed Cache 用 ESI 做局部动态

如果你跑在 LiteSpeed / OpenLiteSpeed 上,最划算的方案是开 ESI。进入「LiteSpeed Cache → 缓存 → ESI」,把 ESI 打开;WooCommerce 的购物车小组件、近期浏览等会被自动切成 ESI 块。私有缓存的 TTL(「缓存」设置里的私有缓存项,默认通常较短)建议保持在 60–180 秒,别设成和公共缓存一样的长值,否则会员改了资料半天不生效。

非 LiteSpeed 环境下想做局部动态,最通用的替代是 AJAX:页面照常缓存成静态,加载完再用 admin-ajax.php 或 REST 路由拉一次用户态数据,把结果填进占位容器。WordPress REST API 自定义接口

第四步:服务器层再兜一层 Cookie 排除

插件层规则只在 PHP 执行时生效,如果 Nginx 还有一层 FastCGI 缓存,需要让两层规则保持一致,否则会出现「插件没缓存但 Nginx 缓存了」的诡异串号:

# http 块:定义哪些请求不该碰共享缓存
map $http_cookie $no_shared_cache {
    default                        0;
    "~*wordpress_logged_in"        1;
    "~*wp-postpass"                1;
    "~*comment_author"             1;
    "~*woocommerce_items_in_cart"  1;
    "~*wp_woocommerce_session"     1;
    "~*edd_items_in_cart"          1;
}

map $request_uri $no_shared_uri {
    default 0;
    "~^/(cart|checkout|my-account|add-to-cart)" 1;
    "~^/wp-json/"                  1;
    "~^/wp-admin/"                 1;
}

# server 块的 PHP 段
location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;

    fastcgi_cache_bypass $no_shared_cache $no_shared_uri;
    fastcgi_no_cache     $no_shared_cache $no_shared_uri;
    add_header X-Cache-Status $upstream_cache_status;
}

fastcgi_cache_bypass 表示不读缓存,fastcgi_no_cache 表示不写缓存,两个都要写——只写前者会出现「请求绕过缓存但响应被存了下去」这种最隐蔽的问题。

第五步:上线前跑一遍回归验证

# 1) 匿名请求:静态页应当 HIT
curl -sI https://example.com/sample-page/ | grep -i 'x-cache-status'

# 2) 带登录 Cookie:必须 BYPASS
curl -sI https://example.com/sample-page/ \
  -b 'wordpress_logged_in_abcdef=tester' | grep -i 'x-cache-status'

# 3) 强一致页面逐条确认
for u in /cart/ /checkout/ /my-account/ /wp-json/wp/v2/posts; do
  printf '%-28s -> ' "$u"
  curl -sI "https://example.com$u" | awk 'tolower($1)=="x-cache-status:"{print $2}'
done

# 4) 批量扫一遍:看有没有页面在带 Cookie 时仍然 HIT
while read -r u; do
  s=$(curl -sI "https://example.com$u" -b 'wordpress_logged_in_abcdef=tester' \
      | awk 'tolower($1)=="x-cache-status:"{print $2}')
  [ "$s" = "HIT" ] && echo "危险:$u 登录态命中共享缓存"
done < url-list.txt

常见踩坑

  1. 只排除了页面 URL,没排除 Cookie:会员入口嵌在全站头部,只有 /my-account/ 被排除没用,所有页面都会带上用户名。Cookie 排除优先级高于 URI 排除。

  2. 缓存键里没有 Cookie,却在响应里 Set-Cookie:命中缓存时响应头里的 Set-Cookie 也会一起被复用,等于把别人的会话 ID 发给了新访客。可以用 fastcgi_ignore_headers 显式忽略,或在写缓存前删掉该头。

  3. 案例: 某站点把「限时折扣倒计时」做成了 PHP 输出的时间戳,缓存 TTL 设成 24 小时。活动开始后倒计时一直停在旧值,用户在结算页看到的价格和倒计时对不上,当天退款 17 笔。后来改成页面缓存 + 前端 JS 读取服务器时间戳重算,问题消失。

  4. ESI 块自身被缓存:ESI 片段的 URL 也走同一套缓存规则,如果片段地址没被排除,ESI 就退化成静态值,看起来「开了 ESI 但没效果」。

  5. 私有缓存不设 TTL,磁盘被吃满:几千个登录用户各存一份 HTML,wp-content/cache 一周涨到 20GB,备份直接失败。

  6. AJAX 补动态内容却被浏览器缓存:请求动态数据的接口必须带 Cache-Control: no-store,否则浏览器会缓存 JSON 结果,用户刷新也不变。

  7. 预加载(Preload)把动态页也爬了一遍:WP Rocket、LiteSpeed 的预加载会遍历站点地图,务必确认 sitemap 里不含 /cart/、/checkout/ 这类地址,否则预加载会直接生成一份带「空购物车」的共享缓存。

  8. CDN 与插件两层规则不一致:插件改了排除规则但 CDN 缓存规则还是旧的,一定在改完后主动清一次 CDN 缓存。WordPress CDN 缓存清除

总结

处理缓存与动态内容的冲突,判断标准只有一条:这份输出对不同的人是否可能不同。可能不同就进排除或私有缓存,绝不可能不同才进共享缓存。

行动清单:先列出全站动态片段清单;再用 Cookie + URI 两条线配置插件与服务器层排除(两层规则必须一致);购物车、结账、账户页强制排除;头部登录态、会员价优先用 ESI 或 AJAX 局部化;私有缓存 TTL 控制在 3 分钟以内;上线前用带 Cookie 与匿名两组请求各跑一遍,确认强一致页面全部 BYPASS。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。