最近一段时间,我一直在排查 WordPress 多语言网站中的缓存问题。
网站目前采用的是:
- WordPress
- Polylang
- W3 Total Cache
- W3TC Page Cache:Disk Enhanced
- W3TC Object Cache:Redis
- 中文站:
www.shuijingwanwq.com - 英文站:
en.shuijingwanwq.com - 后台域名:
admin.shuijingwanwq.com - 中文站 CDN:EdgeOne
- 英文站 CDN:Cloudflare
W3 Total Cache 的 Page Cache 生命周期设置为:
345600 秒也就是:
4 天理论上,只要页面没有被主动清理,一个已经生成的 Page Cache 文件应该可以继续存在几天。
但实际观察到的情况却完全不是这样。
我多次检查后发现,缓存文件基本活不过一天。即使没有执行 W3TC 后台的 Purge All Caches,大量 Page Cache 仍然会突然统一失效。
这篇文章记录的,就是这次从现象、文件系统证据、WordPress Hook,到最后定位出 Polylang 标签同步 和 Yoast SEO Cron 两条全量 Page Cache flush 路径的完整过程。
一、最开始的问题:4 天 TTL 实际上没有意义
W3TC 当前配置中:
pgcache.lifetime = 345600也就是 4 天。
但在检查 Page Cache 文件时,我发现最老的缓存通常都不到一天。
真正应该关注的,其实不是“W3TC 配置里写了几天”,而是:
实际缓存寿命
=
min(
配置 TTL,
下一次提前发生的全量 purge 时间
)如果每天都有某个 WordPress Hook 调用了全量 Page Cache flush,那么即使 TTL 设置成 4 天、7 天甚至更长,也没有实际意义。
所以后续排查的重点变成了:
到底是谁在提前清理 Page Cache?
二、先确认 W3TC 的“全量清理”到底长什么样
W3TC 当前使用的是 Disk Enhanced Page Cache。
缓存目录位于:
/data/wwwroot/www.shuijingwanwq.com/wp-content/cache/page_enhanced/开始排查时,我发现一个非常重要的细节:
W3TC 执行全量 Page Cache flush 时,并不是立即把缓存文件全部 rm 掉,而是先把文件改名成:
*_old例如:
_index_slash_ssl.html会变成:
_index_slash_ssl.html_oldgzip 文件也是如此。
这意味着:
_old文件的 ctime,可以作为“什么时候发生了 Page Cache 失效”的重要证据。
这个细节后来非常关键。
三、第一次抓到全量 Page Cache 失效
一次现场检查中,我发现大量缓存文件在极短时间内统一变成了 _old。
当时的规模大约是:
www:906
en:742总计:
1648而且这些文件在不到 0.1 秒的时间窗口内被连续处理。
这已经基本可以排除:
- 正常用户访问;
- Prime;
- 单个文章更新;
- 普通 TTL 过期;
- CDN;
- 浏览器缓存。
这种特征更符合 W3TC 自己的:
Cache_File_Generic::flush()也就是整棵 Page Cache 目录的全量处理。
四、第一条根因:Polylang 标签同步触发 edited_term
当时我刚运行过一个自己长期使用的标签同步脚本:
php polylang-batch-zh-to-en-tags.php这个脚本的用途是:
- 扫描中文标签;
- 查找是否已经存在对应英文标签;
- 如果不存在,则创建英文语言标签;
- 建立 Polylang 的中英文标签翻译关系;
- 后续再到后台把新生成的英文标签名称改成真正的英文。
某次实际运行结果是:
源语言标签总数: 9451
已处理新标签: 2
已跳过已有翻译: 9449看起来只新增了两个标签。
但这两个标签却触发了整个 Page Cache 的失效。
继续追踪后,调用链逐渐清晰。
五、问题并不只是 wp_insert_term()
最开始容易怀疑的是“新建标签”本身。
但 W3 Total Cache 2.10.6 在当前环境下,对 taxonomy 的相关 Hook 主要是:
edited_term
delete_term它们都直接绑定:
w3tc_flush_posts()而 w3tc_flush_posts() 在当前 Disk Enhanced Page Cache 配置下,并不是“清理相关文章”,而是最终进入:
PgCache_Flush::flush()然后执行整个 Page Cache 的全量 flush。
问题在于:
W3TC 并没有判断当前修改的 taxonomy 到底是什么。
也就是说:
修改文章标签
修改分类
修改 Polylang 内部 term translation taxonomy对 W3TC 来说,都可能被当成:
全部文章缓存都应该失效这对我的网站并不合理。
六、Polylang 翻译关系让问题变得更隐蔽
这次并不是简单的:
创建一个 post_tag
→ 全量 flush真正的流程还包含 Polylang 自己维护的 taxonomy 翻译关系。
生产运行时确认,Polylang 当前用于 term 翻译关系的 taxonomy 是:
term_translations标签同步脚本创建英文标签后,会继续更新 Polylang 翻译组。
其中包括:
save_translations()后续会进入:
wp_update_term()从而触发:
edited_term而 W3TC 在 edited_term 上直接执行:
w3tc_flush_posts()另外,清理某些临时翻译关系时还可能进入:
delete_term同样会再次执行:
w3tc_flush_posts()因此,表面上只是:
新增两个英文标签背后实际上可能已经发起了多次全量 Page Cache flush 请求。
七、为什么这类标签修改不值得清空全站缓存
这里需要先明确业务需求。
我的网站标签数量很多,标签和分类的修改对前台实时一致性的要求并不高。
即使某个标签名称修改以后,极少数已经缓存的页面继续显示几小时甚至几天旧内容,我也可以接受。
相比之下,更重要的是:
不要因为一次很小的 taxonomy 操作,把几千个已经生成好的 Page Cache 全部清掉。
因为当前源站并不是高配服务器。
正常情况下访问链路最好是:
CDN HIT如果 CDN MISS,则:
CDN MISS
→ W3TC Page Cache HIT
→ 不进入 PHP而如果 W3TC Page Cache 被整体清空,则会变成:
CDN MISS
→ W3TC MISS
→ PHP
→ WordPress
→ Redis
→ 数据库在较大访问量下,这种缓存冷启动明显更容易造成 CPU 波动。
因此,我最终决定:
category、post_tag 和 Polylang term translation taxonomy 的修改,不再触发 W3TC 全量 Page Cache flush。
八、第一项修复:taxonomy-aware W3TC flush
没有修改 W3 Total Cache 源码,也没有修改 Polylang 源码。
而是继续使用 MU Plugin。
核心思路是:
先移除 W3TC 原来的:
remove_action( 'edited_term', 'w3tc_flush_posts', 0 );
remove_action( 'delete_term', 'w3tc_flush_posts', 0 );再注册自己的 wrapper。
wrapper 根据 taxonomy 判断:
category
post_tag
term_translations对于这些 taxonomy:
不执行全量 w3tc_flush_posts()对于其他 taxonomy,则继续调用 W3TC 原始行为。
这样做的目的不是“禁止所有 term 缓存刷新”,而是:
只缩小已经确认不需要全站 purge 的 taxonomy 范围。
九、没有直接宣布修复成功,而是做真实生产流程验证
代码部署以后,我没有马上为了测试而批量创建数据,而是直接用日常博客发布流程验证。
为了能够客观观察缓存,我另外增加了一个只读工具:
page-cache-snapshot它会统计每个 Host 的:
- Active HTML 数量;
<1 天;1~2 天;2~3 天;3~4 天;>=4 天;- 最老活跃缓存时间;
_old文件数量;- 最近 5 / 15 / 60 分钟新出现的
_old。
另外还部署了一个临时 tracer:
w3tc-full-flush-tracer.php只要发生:
w3tc_flush_posts或者:
w3tc_flush_all就记录调用栈。
十、完整跑了一次真实双语博客发布流程
随后我完整发布了一篇中英文博客。
验证过程包括:
1. 新建中文文章
操作包括:
- 新建文章;
- 编辑正文;
- 设置分类;
- 设置标签;
- 保存草稿。
结果:
没有发生全量 Page Cache flush2. 发布中文文章
发布以后,出现了少量 _old。
主要是:
首页
Feed
部分分页但最老的历史缓存仍然存在。
Tracer:
0说明这是正常的 URL 级精确失效,而不是全量 purge。
3. 再次运行标签同步脚本
这一次脚本仍然新增了两个标签:
Crawler Hints
网站收录统计:
源语言标签总数: 9453
已处理新标签: 2
已跳过已有翻译: 9451但 Page Cache 前后:
www OLD_COUNT:710 → 710
en OLD_COUNT:618 → 618最老缓存时间完全没变。
Tracer:
0这说明第一次 taxonomy 修复已经在真实生产流程中生效。
十一、后台修改英文标签也没有再清空 Page Cache
随后我把:
网站收录修改成:
Website Indexing这个动作会触发:
wp_update_term()
→ edited_term也正是之前 W3TC 会执行全量 purge 的路径。
修改以后:
www OLD_CTIME_5M = 0
en OLD_CTIME_5M = 0Tracer:
0最老缓存仍然保留。
至此,可以确认:
post_tag后台修改同样不会再把整个 Page Cache 清空。
十二、继续验证英文文章生成和发布
后续又继续走完整的英文翻译流程。
SlyTranslate 创建英文草稿
英文文章 ID:
27481创建英文草稿以后:
www 最老缓存不变
en 最老缓存不变
Tracer = 0没有全量 purge。
正式发布英文文章
发布英文文章后,只出现了极少量 _old:
www:
feed
en:
首页对应普通文件和 gzip 文件,总共只有 4 个。
Tracer 仍然:
0这说明英文文章正式发布也仍然走的是小范围 URL purge。
十三、本以为问题解决了,第二天缓存还是没活过 24 小时
到这里,我一度以为问题已经解决。
但我没有立刻下结论,而是继续保留 tracer。
第二天再次检查时发现:
AGE_1_TO_2D = 0最老缓存又变成了前一天 17:30 左右。
更夸张的是:
www OLD_COUNT = 11466
en OLD_COUNT = 7620显然,又发生了一次全量 flush。
但是这次终于有 tracer 了。
十四、第二个根因:Yoast SEO 的每日 Cron
Tracer 记录到了两次:
w3tc_flush_posts时间分别是:
17:30:05.175
17:30:05.189两个调用栈完全指向同一条路径:
wpseo_detect_default_seo_data
↓
Default_SEO_Data_Cron_Callback_Integration
↓
Options_Helper->set()
↓
WPSEO_Options::save_option()
↓
update_option_wpseo
↓
WPSEO_Utils::clear_cache()
↓
w3tc_flush_posts()
↓
W3TC 全量 Page Cache flush至此,第二条根因已经非常明确。
十五、Yoast 到底更新了什么?
生产环境当前 Yoast SEO 版本是:
28.4进一步检查源码后发现,这个 Cron 只更新两个字段:
default_seo_title
default_seo_meta_desc当时数据库中的值是:
default_seo_title=[27481,27474,27472,27465,27463]
default_seo_meta_desc=[27481,27474,27472,27465,27463]这些数据保存的是:
最近几篇文章中,哪些文章仍然使用默认 SEO title 或 meta description。
它们主要用于 Yoast 后台编辑器里的提醒。
并不会直接改变:
- 前台 SEO title;
- meta description;
- canonical;
- schema;
- sitemap;
- post 内容;
- post meta;
- indexable。
所以为了这两个后台提醒字段:
清空 www + en + admin 的整个 Page Cache明显没有必要。
十六、为什么 Yoast 会触发 W3TC 全量 flush?
Yoast SEO 28.4 中:
WPSEO_Utils::clear_cache()的相关逻辑非常简单:
if ( function_exists( 'w3tc_flush_posts' ) ) {
w3tc_flush_posts();
}
elseif ( function_exists( 'wp_cache_clear_cache' ) ) {
wp_cache_clear_cache();
}也就是说,只要检测到 W3TC:
Yoast option 更新
→ WPSEO_Utils::clear_cache()
→ w3tc_flush_posts()它没有进一步判断:
- 到底改的是哪个 wpseo 字段;
- 是否发生在 Cron;
- 字段是否影响前端;
- 是否真的需要清空所有页面。
十七、Yoast 自己其实也会主动绕过这次 cache flush
这里还发现了一个很有意思的细节。
Yoast 28.4 自己在另一处内部流程中,也使用了类似逻辑:
remove_action(
'update_option_wpseo',
[ 'WPSEO_Utils', 'clear_cache' ]
);
$this->options_helper->set( ... );
add_action(
'update_option_wpseo',
[ 'WPSEO_Utils', 'clear_cache' ]
);也就是说:
Yoast 自己也知道,并不是每一次内部
wpseooption 更新,都值得触发 W3TC 全量 cache flush。
这给后续兼容方案提供了很好的实现依据。
十八、第二项修复:只在这个 Yoast Cron 中暂时移除 callback
最终我没有去修改:
WPSEO_Utils::clear_cache()也没有去动 W3TC 内部 Page Cache callback。
而是新增了一个独立 MU Plugin:
yoast-w3tc-cache-compat.php只处理:
wpseo_detect_default_seo_data这个 Cron。
设计是:
priority 1
→ 临时移除 WPSEO_Utils::clear_cache
priority 10
→ Yoast 正常执行 default SEO data Cron
priority 999
→ 恢复 WPSEO_Utils::clear_cache因此 suppression 只存在于:
wpseo_detect_default_seo_data这一轮 action 内。
不会一直持续到整个:
wp-cron.php请求结束。
这样即使同一个 PHP Cron 请求后面还有其他事件,也不会受影响。
十九、还专门处理了 W3TC 不存在的情况
Yoast 的 clear_cache() 还有 fallback:
wp_cache_clear_cache()因此 MU Plugin 不能在 W3TC 没有加载时仍然把整个 Yoast callback 移除。
最终增加了条件:
function_exists( 'w3tc_flush_posts' )只有确认 W3TC 当前实际存在时才进行 suppression。
这样:
Yoast Cron + W3TC 存在
→ 临时抑制 WPSEO_Utils::clear_cache而:
Yoast Cron + W3TC 不存在
→ 完全不干预 Yoast这样不会意外破坏 Yoast 对其他缓存插件的 fallback。
二十、部署后的 Hook 状态
部署后实际运行时检查:
wpseo_detect_default_seo_data
1
yoast_w3tc_cache_compat_begin_default_seo_data_cron
10
Default_SEO_Data_Cron_Callback_Integration
->detect_default_seo_data_in_recent
999
yoast_w3tc_cache_compat_end_default_seo_data_cron正常请求下:
update_option_wpseo
10 | WPSEO_Options::clear_cache
10 | WPSEO_Utils::clear_cache
10 | 其他 Yoast watchers这说明:
MU Plugin 并没有在 WordPress 加载阶段就删除 Yoast 原有 callback。
只有真正进入目标 Cron 时才会临时处理。
二十一、终于第一次看到 Page Cache 活过 24 小时
2026 年 9 月 8 日晚上再次检查。
这次出现了一个之前一直没有出现过的结果。
中文站:
AGE_LT_1D=6989
AGE_1_TO_2D=181英文站:
AGE_LT_1D=4785
AGE_1_TO_2D=147最老缓存分别仍然是:
www:
2026-09-07 17:30:23
en:
2026-09-07 17:30:35Tracer 仍然只有前一天修复前留下的:
2没有任何新记录。
这也是这次排查以来第一次真正看到:
AGE_1_TO_2D > 0换句话说:
Page Cache 终于第一次真实跨过了 24 小时生命周期。
这说明至少之前每天提前清理 Page Cache 的两条主要路径,已经不再继续把缓存每天重置。
二十二、这里仍然保留一个未完全闭环的验证点
虽然这次 Yoast Cron 的 next run 已经从:
2026-09-08推进到了:
2026-09-09说明当天 Cron 已经正常执行。
Tracer 也没有新增记录。
但这次:
default_seo_title
default_seo_meta_desc两个数组刚好没有变化。
仍然是:
[27481,27474,27472,27465,27463]WordPress 的:
update_option()如果新旧值完全一样,就不会真正执行数据库更新,也不会触发:
update_option_wpseo所以目前最严谨的结论是:
Yoast Cron 兼容 MU Plugin 已通过部署稳定性和自然 Cron 回归测试,Page Cache 也首次存活超过 24 小时。
但还缺最后一块最强证据:
default_seo_* 值发生变化
↓
update_option_wpseo 真正执行
↓
WPSEO_Utils::clear_cache 被临时抑制
↓
Tracer 仍然不增加
↓
Page Cache 继续存活这一步我没有通过手工修改数据库去制造测试条件。
我更倾向于继续等待真实内容更新自然触发。
二十三、这次排查最大的收获:不要只盯着 TTL
这次最值得记住的,其实不是某一段 MU Plugin 代码。
而是:
配置的缓存生命周期,并不等于真实缓存生命周期。
例如:
W3TC Page Cache TTL = 4 天只说明:
缓存最长允许活 4 天并不意味着它真的能够活 4 天。
如果:
第 1 天 Polylang edited_term
→ 全量 flush
第 2 天 Yoast Cron
→ 全量 flush那么实际缓存寿命可能只有:
不到 24 小时这时候继续调大 TTL 是没有意义的。
真正应该查的是:
谁在提前清缓存?二十四、Page Cache、Object Cache 和 CDN 必须分层判断
这次排查也再次说明,缓存问题不能简单归因于“Redis”或者“W3TC”。
至少要区分:
浏览器缓存
CDN
W3TC Page Cache
W3TC Object Cache / Redis
WordPress PHP Runtime
Polylang
Nginx这次两个问题都属于:
W3TC Page Cache 主动失效而不是:
Redis Object Cache 数据污染也不是:
CDN 节点缓存错误如果最开始只是看到“页面又变慢了”,然后直接清 Redis、清 CDN、Purge All Caches,就永远很难知道真正是谁造成了问题。
二十五、目前的两个兼容层保持独立
最终我没有把所有兼容代码都塞进一个 MU Plugin。
现在分别是:
polylang-w3tc-cache-compat.php负责:
Polylang taxonomy
↔
W3 Total Cache以及:
yoast-w3tc-cache-compat.php负责:
Yoast SEO default SEO data Cron
↔
W3 Total Cache这样做有几个好处:
- 职责单一;
- 容易单独停用;
- 容易回滚;
- 升级某个插件时可以单独复核;
- 出现新问题时,不会把不同兼容逻辑混在一起。
二十六、后续还要继续观察
目前并不能因此认为:
以后绝对不会再出现任何 W3TC 全量 Page Cache flush更准确的说法是:
已经确认并修复了两条真实发生过的主要提前失效路径。
我暂时仍然保留:
w3tc-full-flush-tracer.php继续观察:
w3tc_flush_posts
w3tc_flush_all如果后续还有其他插件、Cron 或后台操作触发全量 flush,就可以继续通过调用栈定位。
另外还要继续观察:
AGE_1_TO_2D
AGE_2_TO_3D
AGE_3_TO_4D最终目标是确认正常页面缓存确实能够逐步接近当前配置的:
4 天 TTL而不是再次被其他隐藏路径提前清掉。
二十七、下一轮验证就用这篇文章本身
这篇文章发布以后,我准备直接把它作为下一轮生产测试对象。
后续如果需要补充新的验证结果,就正常编辑并更新这篇已经发布的文章。
然后继续观察:
文章更新
↓
W3TC 是否只做相关 URL 精确 purge
↓
其他旧 Page Cache 是否继续存活同时,这篇文章发布以后,也会改变最近文章集合。
下一次:
wpseo_detect_default_seo_data自然 Cron 再运行时:
default_seo_title
default_seo_meta_desc就更有可能发生真实变化。
如果届时:
option 正常更新
Tracer 仍然没有新增 w3tc_flush_posts
历史 Page Cache 仍然存在那么 Yoast 这条兼容修复就可以完成最后一块真实生产闭环。
总结
这次问题表面上是:
W3 Total Cache 设置了 4 天 Page Cache,但缓存为什么总是活不过一天?
最终却发现并不是 TTL 配置错误,而是至少存在两条提前失效路径:
第一条:
Polylang 标签/分类翻译关系
→ edited_term / delete_term
→ W3TC w3tc_flush_posts()
→ 全量 Page Cache flush第二条:
Yoast SEO
wpseo_detect_default_seo_data Cron
→ update_option_wpseo
→ WPSEO_Utils::clear_cache()
→ w3tc_flush_posts()
→ 全量 Page Cache flush两个问题分别通过独立 MU Plugin 进行了最小范围兼容。
在修复后,真实双语博客发布流程中的:
- 中文草稿;
- 中文发布;
- 标签同步;
- 标签重命名;
- 英文翻译草稿;
- 英文正式发布;
都没有再触发异常的全量 Page Cache flush。
随后 Page Cache 也第一次真正出现:
AGE_1_TO_2D > 0对于生产环境中的缓存问题,我现在越来越倾向于采用一种比较慢、但更可靠的方法:
先留下证据
→ 确认是哪一层缓存
→ 找到真实 Hook
→ 做最小修改
→ 用真实业务流程验证
→ 再继续观察而不是一看到旧内容、CPU 波动或者缓存异常,就直接:
Purge All Caches
清 Redis
清 CDN后者可能暂时让问题消失,却很容易把真正的根因一起清掉。
WordPress 网站维护、性能优化与博客运营咨询
本站已持续运营超过 10 年,累计发布 1000+ 篇原创技术文章,长期实践 WordPress 网站建设、CDN / Cloudflare 配置、缓存优化、Google SEO、广告变现和多语言网站运营。
如果你的 WordPress 网站遇到访问慢、缓存异常、插件冲突、广告不显示、SEO 基础结构混乱、CDN 配置不确定等问题,可以联系我做一次远程技术排查。
适合以下用户:
✅ 个人博客站长
✅ WordPress 网站运营者
✅ 独立开发者与内容创作者
✅ SaaS 产品官网运营团队
✅ 希望优化网站速度与稳定性的站点
服务内容:
✅ WordPress 速度优化
✅ Cloudflare / CDN / 缓存配置排查
✅ 插件冲突与页面异常排查
✅ AdSense 广告显示问题排查
✅ SEO 基础结构检查
✅ 博客运营与商业化咨询
如需了解方案或交流相关问题,请直接联系我,并注明:WordPress 维护咨询。
联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com

