每次发完文章都清一遍Redis,直到我去数了数那个库里有几个键
本文目录
- 我清了半年的那个键值库,里面为什么一直是零个键?
- 四个库,三个满的,一个空的
- 为什么本站一个键都没有
- 先搞清楚东西在哪,再谈清
- 顺手量到的另一件事:淘汰策略从来没生效过
- 命令行里那句重置为什么返回false还不报错?
- 前后两次读数,一个字都没变
- 原因是两套完全独立的实例
- 这台机器上根本不需要那句命令
- 清了里层页面还是旧的,问题出在顺序吗?
- 只清里层,什么都不会发生
- 顺序错了会发生什么
- 完整的顺序长什么样
- 一次全清到底扔掉了什么?
- 304MB,955个页面
- 重建的账该算在谁头上
- 什么时候该全清,什么时候不该
- 我以为在测冷启动,其实已经被别人预热了
- 后台更新和陈旧回退,会把你的清理动作推迟多久?
- 两个开关,一个共同后果
- 状态头六种取值,各说明什么
- 配置文件里写着3600,发出去的为什么是300?
- 十二处硬编码,全都没生效
- 还有一处更早就该发现的矛盾
- 怎么证明缓存真的清掉了?
- 那条命令为什么在脚本里静默失败
- 三层各自的作证命令
- 没有状态头的话,先把它加上
- HTML和图片是两套缓存,清法为什么不同?
- 一张图,一份HTML
- 但它带来一个后果
- 保哥的清缓存四步与作证清单
- 四步
- 作证清单
- 三条给自动化脚本的硬要求
- 常见问题解答
- 怎么快速判断我的站到底启没启用对象缓存?
- 直接把所有库都清掉不是更省事吗?
- 清完缓存网站变慢了,是不是清错了?
- 为什么改了内容之后,有的用户看到新的有的看到旧的?
- 命令行里那句重置既然无效,正确的做法是什么?
- 按URL精准清理和全清,日常该用哪个?
- 权威参考资料
摘要:每次发完文章都要跑一遍清缓存流程,其中一条是清那个键值库的0号库。跑了大半年,这次顺手数了一下:0号库里一直是0个键。真正装着东西的是1号库的2159个键和2号库的8377个键,而那两批键的前缀属于同一台机器上另外两个站点,跟本站毫无关系。同一轮排查里还钉死了第二件事:命令行里那句字节码缓存重置返回 bool(false),执行前后进程池那边的重启计数和命中数一个数字都没变。两件事的失败模式完全一样——不报错、不抛异常、脚本继续往下打印“已完成”。发完文章清缓存,这是每个自建站的人都有的收尾动作。命令通常是从某篇教程里抄来的,抄完就再没看过,因为它从来不报错。
不报错这件事本身就值得怀疑一下。这次我把整条清理链路拆开量了一遍,量出来的东西比预想的难看:四层缓存里,我常年在清的那一层是空的;另一层我以为清掉了,其实那条命令连执行都没执行成功。
下面的数字全部来自2026年7月31日对一台真实生产服务器的实测,包括我自己那台。
我清了半年的那个键值库,里面为什么一直是零个键?
先把这台机器上的缓存分层摆出来。一个典型的自建站,从里往外依次是:字节码缓存(PHP把源码编译成的中间产物)、对象缓存(数据库查询结果,通常放在内存键值库里)、全页缓存(渲染好的整页HTML,放在磁盘上)、然后才是CDN和浏览器。
四个库,三个满的,一个空的
把内存键值库的每个逻辑库都数一遍:
| 逻辑库 | 键数 | 键名前缀 | 属于谁 |
|---|---|---|---|
| 0号库 | 0 | — | 没有任何东西 |
| 1号库 | 2159 | 某站点前缀加下划线 | 同机的另一个站 |
| 2号库 | 8377 | 另一套前缀 | 同机的第三个站 |
| 3号库 | 1 | 某缓存插件的指标键 | 同上 |
我的收尾脚本里那行命令,清的是0号库。0号库常年是0个键。这条命令跑了大半年,每次都成功,每次都清了个寂寞。
更尴尬的是另外两个库里那10536个键。它们的前缀清清楚楚地写着是另外两套系统的对象缓存,跟本站的程序完全不是一码事。如果当初我抄的教程里写的是清全部库而不是清0号库,我早就把隔壁两个站的缓存清光了,而且大概率至今都不知道。
为什么本站一个键都没有
原因很简单,也很容易被自己骗过去:本站根本没有启用对象缓存。装的那批插件里没有一个把查询结果往内存键值库里写,程序层面用的是文件缓存和全页缓存。
那为什么站点看起来一点问题都没有?因为真正挡在前面的是全页缓存那一层。绝大多数请求根本没进到PHP,nginx直接把磁盘上那份现成的HTML甩了出来,压根没有机会去查数据库,自然也就不需要对象缓存。
这就是整件事最危险的地方:一个从来没生效过的缓存层,和一个一直在正常工作的缓存层,从站点表现上完全区分不开。你清一个空库,站点很快;你不清那个空库,站点还是很快。没有任何信号会告诉你这条命令是多余的。
先搞清楚东西在哪,再谈清
逻辑库这个设计本身就是个坑。SELECT命令的官方文档说明了它的语义:一个实例里有多个用编号区分的逻辑库,默认连到0号,而这些库共享同一个实例的内存和配置,只是键空间互相隔离。文档里还专门提到,在集群模式下这个特性是不支持的。
换句话说,多个站共用一个键值实例、靠库号隔离,是一种约定式的隔离——没有权限边界,没有防护,全靠每个人都记得自己该用哪个号。而清库命令本身对库号毫不敏感,FLUSHDB的文档说得很直白,它删除当前所选库的全部键。选错号,删的就是别人的。
所以第一条判据是:动手清之前先数键。三条命令就能问清楚:数一下每个库有多少键、随机抽几个键名看前缀、确认自己的程序到底往哪个库写。这三步花不到一分钟,能省掉半年的无用功,也能避免一次误删。对象缓存那一层的具体配置和命中率该怎么读,Redis对象缓存怎么给WordPress提速?object cache原理与运维实战那篇拆得更细。
顺手量到的另一件事:淘汰策略从来没生效过
既然连上了,把这个实例的运行指标一起拉了出来,又撞见一处配置与现实的落差:
| 指标 | 实测值 |
|---|---|
| 已用内存 | 11.70MB |
| 内存上限 | 4.00GB |
| 淘汰策略 | 按最近最少使用淘汰全部键 |
| 命中次数 | 990878 |
| 未命中次数 | 89966 |
| 过期删除的键 | 41641 |
| 因内存不足被淘汰的键 | 0 |
内存上限设了4GB,实际用了11.7MB,占用率0.29%。而因内存不足被淘汰的键数是 0——这个实例从启动到现在,从来没有因为装不下而扔掉过任何一个键。
这意味着那条淘汰策略配置一次都没有被执行过。它配成什么都一样,因为触发它的前提条件从未出现。而绝大多数教程会花很大篇幅讲这几种策略怎么选,讲得都对,只是对这台机器毫无意义。
命中率倒是个有用的数字:990878除以总请求数得到 91.7%,不算差。另外那41641个过期删除的键说明有效期机制在正常工作——键是自己到点走的,不是被挤走的,这是健康的样子。
这一节想说的其实跟缓存无关:一份配置是不是合理,不能只看它写得对不对,还得看它有没有被执行过。没被执行过的正确配置,和写错了的配置,在系统表现上一样看不出来。
命令行里那句重置为什么返回false还不报错?
第二件事比第一件更普遍,因为它出现在几乎每一份部署脚本里。
前后两次读数,一个字都没变
字节码缓存的重置函数,在命令行里跑一次:
php -r 'var_dump(@opcache_reset());'
bool(false)返回 false。不是异常,不是警告,就是一个安静的false,而绝大多数脚本写的是 php -r 'opcache_reset();'——连返回值都不看。
为了确认它到底有没有影响到真正干活的那一侧,我在网站目录里临时放了一个探针脚本,通过HTTP请求读取进程池里的状态。执行重置前后各读一次:
| 指标 | 重置前 | 命令行重置后 |
|---|---|---|
| 是否启用 | true | true |
| 已缓存脚本数 | 4230 | 4230 |
| 命中次数 | 415623 | 415623 |
| 未命中次数 | 4231 | 4231 |
| 命中率 | 98.99% | 98.99% |
| 已用内存 | 245.8MB | 245.8MB |
| 手动重启计数 | 1 | 1 |
七个指标,一个都没动。最能说明问题的是最后一行——手动重启计数专门用来记录重置被调用了几次,它停在1上纹丝不动,说明那句命令根本没碰到这个进程池。
原因是两套完全独立的实例
查一下配置就明白了:
opcache.enable => On => On
opcache.enable_cli => Off => Off字节码缓存的配置项手册里,enable_cli 这一项的默认值就是关闭。而 重置函数的手册页写明它的作用是重置整个缓存的内容——问题在于“整个”指的是当前这个进程能看到的那份缓存。
命令行跑的PHP和进程池里的PHP是两个完全独立的进程,各自有各自的共享内存段。命令行那边压根没开缓存,所以它连一份可重置的东西都没有,函数老老实实返回false;而进程池那边的4230个脚本、245.8MB内存,站在命令行的角度是完全够不着的。
这个坑的杀伤力在于它的普遍程度。“部署完跑一句重置”几乎是所有PHP部署教程的标准收尾,而在默认配置下,这一句在命令行里执行等于什么都没做。
这台机器上根本不需要那句命令
还有个反转。同一份配置里还有两行:
opcache.validate_timestamps => On => On
opcache.revalidate_freq => 60 => 60启用了时间戳校验,间隔60秒。意思是改完文件之后最多60秒,进程池会自己发现文件变了并重新编译,不需要任何人手动干预。
所以这条链路上同时存在两个错误认知:一是以为那句命令生效了(其实没有),二是以为需要它(其实不需要)。两个错误刚好互相抵消,于是站点一直正常,也就一直没人去查。
真正需要手动重置的场景只有一种:关掉时间戳校验以换取性能。那种配置下改完文件必须显式重置,而且必须在进程池内部执行——要么通过一个HTTP端点触发,要么直接重载进程池。字节码缓存这一层的完整调优思路,PHP OPcache字节码缓存怎么调才能让站点真正快起来那篇讲过取舍。
清了里层页面还是旧的,问题出在顺序吗?
这是清缓存里最反直觉的一件事:清理的顺序必须跟读取的顺序相反,否则你会亲手把旧内容重新固化一遍。
只清里层,什么都不会发生
做一组对照。先把三个页面预热到全页缓存里,然后只清对象缓存那一层,全页缓存原封不动,再打这三个URL:
| 阶段 | 全页缓存文件数 | 首字节(秒) |
|---|---|---|
| 预热完成(热态) | 4 | 0.2299 / 0.2337 |
| 清完对象缓存 | 4(没变) | 0.2125 / 0.6817 |
| 再看对象缓存键数 | 仍然是0,说明这几个请求根本没走到那一层 | |
首字节几乎没有变化,全页缓存文件数一个没少。你清了里层,但请求压根到不了里层——它在外层就被拦住返回了。
反过来,把全页缓存清掉再打同样的URL:
| 页面 | 清空后第1次(冷) | 第2次 | 第3次 | 冷热比 |
|---|---|---|---|---|
| 页面A | 0.9898 | 0.2339 | 0.2334 | 4.24 |
| 页面B | 0.8756 | 0.2135 | 0.2301 | 4.10 |
| 页面C | 1.2327 | 0.2319 | 0.2332 | 5.32 |
冷启动比命中慢4到5倍,这才是请求真正落回PHP的样子。
顺序错了会发生什么
把这两组数据合起来推演一下,就能看清顺序为什么要紧。
假设你改了内容,然后先清外层(全页缓存)、再清里层(对象缓存)。这两条命令之间总有个时间差,哪怕只有一秒。在这一秒里,任何一个访客或者爬虫敲进来,会发生什么?
- 外层已经空了,请求穿透到PHP。
- PHP去查对象缓存——里层还没清,它拿到的是旧数据。
- PHP用旧数据渲染出一个页面,返回。
- nginx把这个用旧数据渲染的页面写进全页缓存。
- 你这才执行第二条命令,把里层清了。
结果是:里层干净了,外层刚刚被灌进去一份新鲜出炉的旧内容,而且带着完整的有效期。你的清缓存动作,亲手把旧数据的寿命续了一轮。
而按正确顺序(先里层后外层)就不会有这个窗口:里层清完之后,外层还挡着,这段时间里没有任何请求能穿透进来触发重建;等外层也清掉时,里层已经是空的,重建出来的必然是新内容。
所以规则可以压成一句话:清理顺序必须与读取链路相反,从最里层往最外层清。读取是从外往里穿透,清理就得从里往外剥。
完整的顺序长什么样
把这台机器上的四层按正确顺序排开:
| 顺序 | 层 | 动作 | 是否必需 |
|---|---|---|---|
| 1 | 字节码缓存 | 重载进程池,或依赖时间戳校验自动过期 | 只在关闭时间戳校验时必需 |
| 2 | 对象缓存 | 清对应库号(先确认是哪个号) | 只在真的启用了才必需 |
| 3 | 全页缓存 | 删缓存目录,或用清理模块按URL精准删 | 必需 |
| 4 | CDN与边缘 | 调接口刷新,或等有效期自然到点 | 看有没有挂CDN |
浏览器那一层不在这个列表里,因为你清不了别人的浏览器——那一层只能靠响应头里的指令提前约定好行为,事后无法补救。这也是为什么HTML的 max-age 通常要设成0:你唯一能确保事后可清的,是自己这一侧。反向代理层的清理和过期怎么配,Nginx proxy_cache反向代理缓存怎么配那篇有更细的参数。
一次全清到底扔掉了什么?
“清缓存”听起来是个免费动作。量一下就知道它不免费。
304MB,955个页面
执行清空之前,全页缓存目录的状态:
| 指标 | 数值 |
|---|---|
| 缓存文件数 | 955 |
| 占用磁盘 | 304MB |
| 最老的一份 | 约70分钟前生成 |
| 最新的一份 | 执行清理前2秒生成 |
一条 rm -rf 下去,955变成0。955份已经渲染好的HTML全部作废,每一份都是之前某个访客用真实等待时间换来的。
清完之后打了3个URL,目录里只回来4个文件。这个数字很直观地说明了重建是多么缓慢的过程:缓存是被访客一个一个填回来的,不是自己长出来的。
重建的账该算在谁头上
量一下重建成本:清空之后连续请求5个页面,总耗时 1691毫秒,平均每个页面338毫秒。
按这个速率往外推,把955个页面全部重建需要大约 323秒的纯服务器处理时间。但这323秒不是服务器在后台默默还清的——它被拆散成955份,分给了955个倒霉的访客,每人多等0.3到1秒。
而这955个页面本身也说明了点别的:全站文章一千七百多篇,缓存里只有955份是热的,约占56%。剩下四成多的页面本来就是冷的,谁撞上谁买单,这跟清不清缓存没关系,是长尾内容的固有成本。
什么时候该全清,什么时候不该
由此得到一条相当实用的判据:
- 改了单篇内容——只清那一个URL。用清理模块按键精准删,代价接近零。
- 改了模板或全局配置——必须全清,因为影响面覆盖所有页面,没有别的办法。
- 不确定影响面——先想清楚再动手。全清一次的代价是300多秒的用户等待被摊到几百个人头上,不是免费的。
- 调试时反复全清——换个思路,用带随机参数的地址绕过缓存来验证,别拿生产缓存当调试环境。
全页缓存的键怎么设计才能做到按URL精准清理,Nginx fastcgi_cache全页缓存怎么配那篇有完整配置。
我以为在测冷启动,其实已经被别人预热了
前面那张冷热对照表里,其实还有一个页面没列进去,因为它的数据自相矛盾:清空缓存之后第一次请求,首字节 0.2332 秒,跟热态的0.2358、0.2338、0.2342几乎一模一样。
按理说清空之后第一次必然是冷启动,应该在0.9秒上下。它没有。
唯一的解释是:在我执行清空和发出第一个请求之间的那一秒里,别人已经把它请求过一遍了。可能是爬虫,可能是真实访客,也可能是某个监控。那个人替我承担了冷启动的代价,等我的测量请求到达时,缓存已经重建好了。
这条对做测量的人是个硬提醒:在一个有真实流量的生产站上,你没有办法独占一个干净的观测窗口。清空到测量之间的每一毫秒,都可能有别人在改变你要观测的状态。
补救办法有三个,按可靠性排:用一个带随机参数、别人不可能撞上的地址来测;把清空和测量之间的间隔压到最小;或者干脆多测几组,用中位数把这类污染筛掉。我这次是第三种——四组里三组正常、一组被污染,所以那一组的结论只能弃用。
后台更新和陈旧回退,会把你的清理动作推迟多久?
还有两个开关会让“清缓存”这件事的效果变得不那么立竿见影,而它们通常是默认配置的一部分,没人专门去开。
两个开关,一个共同后果
这台机器的全页缓存配置里有这么两行:
fastcgi_cache_background_update on;
fastcgi_cache_use_stale error timeout updating invalid_header http_500 http_503;第一行的意思是:缓存过期之后,先把过期的那份发给用户,同时在后台悄悄发起一个请求去更新它。第二行的意思是:在列出的那几种情况下(后端出错、超时、正在更新、返回500或503),允许继续使用过期的缓存。
两个开关的出发点都是好的——它们让用户永远不用站着等重建,也让后端偶发抖动不至于变成用户可见的错误页。代价是:内容更新的可见时刻,被推迟了整整一轮。
具体推迟多久取决于谁先到。如果过期后第一个到达的请求触发了后台更新,那么这个人拿到的是旧的,下一个人才拿到新的。换句话说,永远有至少一个用户会看到你以为已经更新掉的内容。
状态头六种取值,各说明什么
正因为有这些机制,那个缓存状态头就不只有MISS和HIT两种值。服务器模块的官方文档里定义了完整的取值表,实际排查时会遇到的是这几个:
| 取值 | 含义 | 看到它该想什么 |
|---|---|---|
| MISS | 没命中,这次是真的走了后端 | 清理生效了,或这个页面第一次被访问 |
| HIT | 命中了有效期内的缓存 | 正常状态 |
| EXPIRED | 命中了但已过期,这次重新取了 | 有效期偏短,或访问太稀疏 |
| UPDATING | 过期了,正在后台更新,先给你旧的 | 你看到的是旧内容 |
| STALE | 后端出问题,回退给你过期的那份 | 后端可能正在故障 |
| BYPASS | 规则命中,这次绕过缓存 | 检查绕过条件是不是写宽了 |
验收清理效果时,只有MISS才算数。看到UPDATING或STALE说明你拿到的还是旧内容,这时候下结论说“清了没用”是冤枉的——再打一次通常就正常了。
看到BYPASS则要往另一个方向查:这个页面根本没在缓存里,你清什么都不会有变化。常见原因是绕过条件里包含了登录状态、特定的查询参数或者某些Cookie,而你恰好带着这些东西在测。用一个干净的客户端重测,是排除这种误判最快的办法。
配置文件里写着3600,发出去的为什么是300?
这是这轮排查里最意外的一条,属于顺手撞见的。
十二处硬编码,全都没生效
在服务器配置目录里搜一下缓存指令,找到 12处硬写的这一行:
add_header Cache-Control "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400" always;而线上实际发出去的响应头是:
cache-control: public, max-age=0, s-maxage=300, stale-while-revalidate=864003600变成了300,差12倍。
原因是配置里还有两行用变量的写法,位置更靠后、作用域更具体,于是它赢了。真正生效的值藏在那个变量的定义里,而不是那12处看起来言之凿凿的硬编码里。
这条的教训跟清缓存本身无关,但同源:配置文件里写着什么,跟线上实际发出去什么,是两个必须分别验证的事实。唯一可靠的确认方式是发一个真实请求把响应头打出来看——这类沉默的配置差异该怎么系统性地查,curl -I查了一圈说这站没配缓存头,换成GET之后那五个头全在那篇专门讲了工具本身的坑。
还有一处更早就该发现的矛盾
同一份配置里,全页缓存的有效期是这么写的:
fastcgi_cache_valid 200 301 302 5m;
fastcgi_cache_valid 404 1m;服务器自己只缓存 5分钟,却通过响应头告诉CDN缓存60分钟(那12处硬编码的本意)。RFC 9111对s-maxage的定义写明它专门给共享缓存用,并且会覆盖max-age,也就是说CDN认的是这个值。
两个数字不一致会导致一个尴尬局面:源站上那份5分钟就过期了,CDN那份还要留55分钟。这段时间里,你在源站上做的任何清理动作,对已经到达CDN的那一份都毫无作用。这也是为什么清缓存必须把CDN那一层单独列进流程,而不是清完源站就以为完事了。CDN层的有效期分层该怎么设计,CDN边缘缓存到底怎么配才不踩坑?回源、TTL分层与缓存键实战那篇有一套现成的分层方案。
怎么证明缓存真的清掉了?
前面所有的坑都指向同一个根因:清理动作没有回执。命令跑完打印一行“已完成”,那行字是脚本自己写的,不是缓存告诉你的。
那条命令为什么在脚本里静默失败
顺着这个思路,我把自己的收尾脚本又查了一遍,发现了第三个坑,而且这次是彻底的静默失败。
非交互方式远程执行命令时,环境变量里的可执行路径只有四个目录:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin而这台机器上那个键值库的客户端程序装在一个自定义目录下,不在这四个里面。于是:
$ redis-cli -n 0 DBSIZE
bash: redis-cli: command not found # 退出码 127
$ /www/server/redis/src/redis-cli -n 0 DBSIZE
0 # 退出码 0关键在下面这一步。当这条命令被分号串在一长串收尾命令里时:
$ redis-cli -n 0 FLUSHDB; echo "缓存已清完毕"
bash: redis-cli: command not found
缓存已清完毕命令失败了,成功提示照常打印。因为分号只表示“接着执行下一条”,它不看上一条的退出码。整个批处理返回0,日志里一片祥和。
这个坑跟前面那个字节码重置返回false的坑,是同一个失败模式的两个实例:失败被吞掉,脚本继续,输出看起来一切正常。它们能潜伏这么久,唯一的原因就是没有人去验证结果。
三层各自的作证命令
回执必须来自被清理的对象本身,而不是执行者的自述。三层各有各的作证方式:
| 层 | 清理前后各跑一次 | 合格的回执 |
|---|---|---|
| 对象缓存 | 数当前库的键数 | 数字从N变成0 |
| 全页缓存 | 数缓存目录的文件数 | 数字从N变成0 |
| 字节码缓存 | 通过HTTP读进程池里的手动重启计数 | 计数加1,已缓存脚本数归零 |
| CDN | 请求一次看边缘状态头 | 从HIT变成MISS |
最后一行本站是现成的。清空全页缓存后连打四次同一个URL,把状态头一起记下来:
| 第几次 | 缓存状态头 | 首字节(秒) |
|---|---|---|
| 1 | MISS | 0.4858 |
| 2 | HIT | 0.2340 |
| 3 | HIT | 0.2331 |
| 4 | HIT | 0.2337 |
第一次MISS、耗时0.4858秒,之后三次全部HIT、稳定在0.233秒左右。这四行是无可辩驳的证据链:清理确实生效了(第一次真的没命中),重建确实发生了(第二次开始命中),效果确实存在(差2.1倍)。
没有状态头的话,先把它加上
本站能这么方便地作证,是因为配置里有这么一行:
add_header X-FastCGI-Cache $upstream_cache_status always;nginx的FastCGI模块文档里定义了这个变量的取值,常见的有MISS、HIT、EXPIRED、BYPASS、UPDATING、STALE等。把它挂成一个响应头,成本几乎为零,收益是此后所有的缓存问题都变成了一个可以一眼看穿的事实,而不是靠掐表猜。
如果你的站现在没有这一行,我的建议是把它当成清缓存流程的第0步:先让缓存能说话,再谈怎么清它。浏览器和CDN那一侧的对应信号,浏览器HTTP缓存头怎么配?让回头客秒开又不犯改了不更新的事故那篇也整理过一份。
HTML和图片是两套缓存,清法为什么不同?
同一个站里,两类资源走的是完全不同的缓存策略,因此“清缓存”对它们意味着完全不同的动作。
一张图,一份HTML
把同一个页面和它里面的一张封面图,响应头并排放:
| 响应头 | HTML页面 | 封面图 |
|---|---|---|
| Cache-Control | public, max-age=0, s-maxage=300, stale-while-revalidate=86400 | public, max-age=31536000, immutable |
| ETag | 无 | 有 |
| Last-Modified | 无 | 有 |
| Vary | Accept-Encoding | 无 |
| 浏览器缓存时长 | 0秒 | 一年,且声明不可变 |
差别是刻意设计的,而且设计得对。HTML随时可能更新,所以浏览器端设成0、只让共享缓存留5分钟;图片一旦上传就不会变,所以给一年并声明不可变,浏览器连条件请求都不用发。
但它带来一个后果
immutable 这个指令的含义是“在有效期内我保证不变,你连问都不用问”。后果是:这张图一旦被用户缓存,你在服务器上把它换掉也没用,那一年之内这个用户看到的都是旧图。
所以图片这一层的“清缓存”根本不是清,而是换名字——改文件名或者加内容指纹,让它变成一个全新的URL。这是唯一可靠的办法。
顺带说一下HTML那边没有ETag和Last-Modified的事。这对动态页面是合理的(每次生成的字节可能都有细微差别,发ETag反而会造成误判),代价是无法用条件请求省下重复传输。这笔账在抓取量大的站上不小,但它跟清缓存是两个独立的话题,多层缓存对首字节和抓取的联合影响,TTFB怎么优化才不白费:多层缓存如何同时左右Core Web Vitals与Google抓取那篇算过完整的账。
保哥的清缓存四步与作证清单
把这一轮踩出来的东西压成一套可以直接照抄的流程。
四步
- 先摸清有哪几层,各在哪。数一下每个键值库有多少键、看键名前缀是谁的、确认全页缓存目录在哪、确认字节码缓存是不是开着时间戳校验。这一步只做一次,做完写进文档。
- 从最里层往最外层清。字节码 → 对象 → 全页 → CDN。顺序反了会把旧内容重新固化。
- 每清一层,立刻取回执。前后各数一次,数字必须变。脚本自己打印的成功提示一律不算。
- 用真实请求做最后验收。打一次目标URL,看缓存状态头是不是MISS,再打一次看是不是变回HIT。这两次的组合能同时证明“清掉了”和“又建好了”。
作证清单
| 检查项 | 合格标准 | 不合格时最可能的原因 |
|---|---|---|
| 对象缓存键数 | 清理后为0 | 清错了库号,或本来就没启用 |
| 全页缓存文件数 | 清理后为0 | 路径写错,或权限不足 |
| 字节码手动重启计数 | 加1 | 在命令行执行的,没碰到进程池 |
| 缓存状态头 | 先MISS后HIT | 没配状态头,或请求被绕过 |
| 页面上那处改动 | 肉眼可见 | CDN那层没清 |
| 命令退出码 | 全部为0 | 路径没写全,命令根本没执行 |
三条给自动化脚本的硬要求
第一,所有命令写全路径。非交互环境的可执行路径跟你登录后看到的不是一回事,装在自定义目录里的程序一律找不到。这一条能挡掉本文里的第三个坑。
第二,用 && 而不是分号串联。分号不看退出码,前一条炸了后一条照跑;换成 &&,任何一步失败整条链就停在那里,比事后翻日志强得多。
第三,把回执打进日志,而不是打成功提示。日志里应该出现的是“键数2159 → 0”“文件数955 → 0”这样的数字对,而不是“缓存已清理完毕”。前者无法伪造,后者只是个字符串。
这三条加起来的成本,大概是给收尾脚本多写十几行。而它们要挡的,是本文里那三个各自潜伏了大半年的坑——清了个空库、重置了个不存在的缓存、命令根本没执行。三个坑的共同点是都不会报错,所以它们只能被主动验证发现,等不到自己暴露的那一天。
顺带一提,做完这轮排查我把自己的收尾脚本改了:那条清0号库的命令直接删掉了,因为它清的东西根本不存在;那条命令行重置也删掉了,因为时间戳校验会替我干;剩下的全页缓存清理换成了全路径写法,后面跟一条数文件数的命令做回执。脚本短了两行,可信度高了不止两倍。
常见问题解答
怎么快速判断我的站到底启没启用对象缓存?
两步。第一步数键:连上键值库,逐个库数键数并抽样看键名前缀,如果没有一个前缀跟你的程序对得上,那就是没启用。第二步看程序:查一下配置文件里有没有指向键值库的连接参数,以及有没有装对应的缓存插件。两步结论一致才可信——我这次就是只看了插件列表以为装了,实际上装的那批插件里没有一个往键值库写东西。
直接把所有库都清掉不是更省事吗?
非常不建议,尤其在共享服务器上。清全部库的命令会把这个实例里所有逻辑库的键一次删光,而多个站共用一个实例、靠库号隔离是很常见的部署方式。本文实测的这台机器上就有另外两个站的一万多个键躺在1号和2号库里,一条命令下去它们全没了,而且对方大概率查不出是谁干的——因为他们的站不会报错,只会突然变慢一阵子。
清完缓存网站变慢了,是不是清错了?
不是,这是正常现象,而且是可预期的。本文实测的冷热比是4到5倍,也就是说清完之后头几分钟,每个第一次访问的页面都会慢4到5倍。缓存是被访客一个一个填回来的,955个页面全部重建需要大约323秒的服务器处理时间,这些时间会摊到几百个访客头上。如果你的站流量大、页面多,建议在低峰期清,或者改用按URL精准清理而不是全清。
为什么改了内容之后,有的用户看到新的有的看到旧的?
最常见的原因是漏清了某一层,而不同用户命中的层不同。典型场景:你清了源站,但CDN那份还在有效期内,于是命中CDN的用户看到旧的、回源的用户看到新的。第二常见的是浏览器缓存——如果HTML的有效期没设成0,老访客手上那份要等自然过期。第三种是共享缓存和私有缓存的有效期设成了不一致的值,本文实测的这台机器上就有这个问题:配置里写着一小时,实际发出去的是五分钟。
命令行里那句重置既然无效,正确的做法是什么?
看你为什么要重置。如果时间戳校验是开着的(多数默认配置都是),那就什么都不用做,改完文件最多等一个校验间隔就自动生效了。如果关掉了校验以换性能,那就必须让重置发生在进程池内部:要么放一个受保护的HTTP端点、部署时请求一次,要么直接重载进程池。判断自己属于哪种情况,查一下配置里时间戳校验那一项是开还是关,一行就够。
按URL精准清理和全清,日常该用哪个?
日常绝大多数情况用精准清理。判据很简单:看这次改动的影响面。改了一篇文章的正文,影响面就是那一个URL加上可能引用它的列表页,精准清几个键就够了。改了模板、导航、全局样式或者任何会出现在每个页面上的东西,影响面是全站,那就只能全清。介于两者之间的(比如改了某个分类的描述)可以按前缀批量清。真正要避免的是养成“出问题先全清一遍”的习惯,那等于每次都让几百个访客替你的不确定性买单。
权威参考资料
本文标题:《每次发完文章都清一遍Redis,直到我去数了数那个库里有几个键》
本文链接:https://zhangwenbao.com/multilayer-cache-purge-order-and-receipt.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0
← 上一篇
那一跳丢包96.7%,终点却一个包都没丢下一篇 →
没有了