使用Varnish缓存带分页的博客搜索结果页的最佳实践
作为常年和Varnish打交道、踩过无数缓存坑的老司机,我来给你拆解下带分页的博客搜索结果页该怎么高效缓存——毕竟这种场景搞不好要么缓存命中率低到感人,要么用户刷到旧内容骂娘,得平衡好缓存效率和内容新鲜度。
一、先搞定最核心的:精准设计缓存键
搜索结果的唯一性完全由query(搜索词)和page(页码)两个参数决定,所以第一步要确保Varnish把这俩参数纳入缓存键的计算,同时避免重复缓存相同内容的请求。
1. 标准化参数顺序,避免冗余缓存
用户有时候会传不同顺序的参数(比如?query=varnish&page=2和?page=2&query=varnish),默认Varnish会把它们当成不同的请求缓存两次,纯纯浪费资源。可以在vcl_recv里标准化参数顺序:
sub vcl_recv { # 针对搜索接口,强制把query参数放在page前面,统一缓存键 if (req.url ~ "^/search\?") { set req.url = regsuball(req.url, "(page=[^&]+)&(query=[^&]+)", "\2&\1"); # 去除重复的参数(比如用户不小心传了两个page参数) set req.url = regsuball(req.url, "&page=[^&]+(?=&page=)", ""); } }
2. 确保缓存键包含必要维度
默认的vcl_hash已经包含req.url,但如果你有多域名或者多语言的场景,记得把req.http.host和req.http.Accept-Language也加进去,避免跨域名/跨语言的缓存混乱:
sub vcl_hash { hash_data(req.http.host); hash_data(req.url); # 如果有语言区分,加上Accept-Language if (req.http.Accept-Language) { hash_data(req.http.Accept-Language); } return (lookup); }
二、后端该返回的缓存控制头
后端要给Varnish明确的缓存指令,别让它猜!推荐返回这样的响应头:
Cache-Control: public, s-maxage=3600, max-age=60public:告诉Varnish这是公共可缓存的内容,不用因为Cookie之类的跳过缓存s-maxage=3600:给Varnish的缓存时间(比如1小时,根据你的内容更新频率调整,更新勤就设短点)max-age=60:给浏览器的缓存时间(比如1分钟,避免浏览器存太久导致用户看不到新内容)
- 额外加分项:返回
ETag或Last-Modified头,这样Varnish可以在缓存过期后用If-None-Match/If-Modified-Since去后端验证,如果内容没变化,直接返回304,省带宽又快。
三、Varnish端的针对性配置
光靠后端头还不够,Varnish里还要做一些适配,让分页缓存更丝滑:
1. 缓存空搜索结果
很多人会忽略空结果的缓存——比如用户搜了一个完全没匹配的词,每次都回源太浪费。可以在vcl_backend_response里给空结果设缓存:
sub vcl_backend_response { if (req.url ~ "^/search\?" && beresp.status == 200 && beresp.http.Content-Length == "0") { set beresp.ttl = 1h; set beresp.http.Cache-Control = "public, s-maxage=3600"; } }
2. 给搜索缓存打标签,方便批量失效
当你发布了新博客,可能需要让相关的搜索结果缓存失效。给所有搜索请求的缓存加个标签,后面要刷新的时候一键搞定:
sub vcl_backend_response { if (req.url ~ "^/search\?") { set beresp.http.X-Cache-Tag = "search_results"; # 更精准的话,可以给特定搜索词打标签,比如query=varnish的加tag: search_varnish if (req.url ~ "query=([^&]+)") { set beresp.http.X-Cache-Tag = beresp.http.X-Cache-Tag + ", search_" + regsub(req.url, "^.*query=([^&]+).*$", "\1"); } } }
然后要刷新时,用Varnish admin命令:
# 刷新所有搜索结果缓存 varnishadm ban obj.http.X-Cache-Tag ~ search_results # 只刷新和varnish相关的搜索缓存 varnishadm ban obj.http.X-Cache-Tag ~ search_varnish
3. 过滤无关的Cookie
如果你的搜索结果和用户登录状态无关(比如不管登不登录,搜索结果都一样),那就在vcl_recv里去掉Cookie,避免Varnish因为Cookie跳过缓存:
sub vcl_recv { if (req.url ~ "^/search\?" && req.http.Cookie !~ "user_preference") { # 这里的user_preference是指如果有影响搜索结果的Cookie才保留,否则都清掉 unset req.http.Cookie; } }
四、分页缓存的最佳实践
- 预缓存热门搜索词的前几页:如果某些搜索词(比如你的博客高频话题)的前3页访问量特别大,可以在内容更新后主动预热缓存——比如用curl循环请求这些页面,让Varnish提前把缓存建好,用户访问时直接命中。
- 处理非法分页请求:比如用户传了
page=0或者超过最大页数的page=1000,后端返回404或者重定向到page=1,这些响应也要缓存,避免每次都回源处理无效请求。 - 分离个性化内容:如果搜索结果里有个性化模块(比如“你可能喜欢”),别把这部分和公共搜索结果混在一起!最好的做法是:公共的搜索列表缓存,个性化部分用前端AJAX单独请求,这样既不影响缓存命中率,又能保留个性化体验。
五、避坑提醒
- 别乱加
private在Cache-Control里:加了这个Varnish会直接跳过缓存,白忙活一场。 - 别设置过长的
s-maxage:如果你的博客一天更好几次,设1小时就够了,不然用户会看到旧内容;如果是静态博客,设个1天甚至1周都没问题,但要配好失效机制。 - 别忘了
Vary头:如果搜索结果根据语言(Accept-Language)或者设备类型(User-Agent)返回不同内容,后端要返回Vary: Accept-Language, User-Agent,Varnish会根据这些头区分缓存键,避免内容混乱。
备注:内容来源于stack exchange,提问作者ahmedbhs




