Redis 缓存常见使用方式

改完首页布局传上服务器,无痕窗口和清缓存都试过,页面还是老的。文件名带 hash 也没用,问题出在 Nginx 给 HTML 设了缓存头。这篇顺便记了缓存失效为什么比加缓存难,以及什么情况下才真该上 Redis。
我先加了缓存,然后首页三天没更新
那天下午我改完前端的首页布局,本地构建出来是 index-CLk3x9ap.js,跑了一遍没问题,就按流程传上服务器。
传完我在浏览器里刷新,还是老的页面。我以为是自己浏览器缓存,开了个无痕窗口,还是老的。把浏览器的缓存全清了,还是老的。
到这一步我有点慌,因为文件明明传上去了。
排查:文件在,nginx 不认
先确认服务器上的文件。
ssh root@124.221.6.10 "grep -o 'assets/index-[^\"]*' /www/wwwroot/www.tiana.com.cn/index.html"输出是 assets/index-CLk3x9ap.js,新文件在位。
再看 nginx 实际吐给客户端的是什么。
curl -s https://www.tiana.com.cn/ | grep -o 'assets/index-[^"]*'输出还是老的 index-B2gyCNsb.js。
同一台机器上,文件是新的,nginx 返回的是旧的。中间只隔了一个 nginx 进程。
我第一反应是缓存目录。翻配置,站点根目录开了 open_file_cache:
open_file_cache max=1000 inactive=60m;
open_file_cache_valid 30m;
open_file_cache_min_uses 2;这就是答案。nginx 把静态文件的元信息(inode、mtime、大小)缓存 30 分钟。我用 rsync 覆盖文件的时候,rsync 的默认行为是直接在原 inode 上写,mtime 会变,但 nginx 那份缓存里存的是老的元信息,它认为自己手上的描述符还是有效的,就一直返回老内容。
改法:部署最后一步必须 reload
修起来很简单,nginx -s reload 一下,缓存重建,页面立刻变新。
但我不能接受「每次部署记得手动 reload」这种靠记忆的方案。所以我把它写进了部署脚本的固定步骤里,同步完文件就 reload,不给它留余地。
# 部署脚本第 6 步(固定,不能省)
rsync -az frontend/dist/ $R:/www/wwwroot/www.tiana.com.cn/
chown -R www:www /www/wwwroot/www.tiana.com.cn
nginx -s reloadreload 是平滑的,不会中断正在处理的请求,代价可以忽略。这个坑我踩过一次之后,再看别人写的「部署完没生效」的问题,基本都先怀疑这一层。
应用层我也加了缓存,但不是 Redis
同一周我还在纠结另一件事:要不要给后端上个 Redis。
诱因是访问记录那张表。每来一次接口调用就写一条,一个月 4153 条,文件 1.9 MB。前端查访问日志的时候,后端要把整个列表读进内存里过滤排序再分页。
我去看了下项目的真实负载。单进程 uvicorn,一天最多两三千次接口调用,峰值也就每秒几次。这种量级,Redis 装上去省下的那点 IO,还不够我维护一个额外服务的成本。
所以我用的是最老实的办法,进程内的字典缓存。
import time
from threading import RLock
class TTLCache:
def __init__(self, ttl=300):
self.ttl = ttl
self._data = {}
self._lock = RLock()
def get(self, key):
with self._lock:
item = self._data.get(key)
if not item:
return None
value, expire_at = item
if expire_at < time.time():
self._data.pop(key, None)
return None
return value
def set(self, key, value, ttl=None):
with self._lock:
self._data[key] = (value, time.time() + (ttl or self.ttl))流程没变:先查缓存,没有就查数据源,查完写回缓存。
真正的坑不在缓存本身,在怎么让它失效。
缓存失效才是那个难的地方
我最早给站点配置加的缓存是十分钟。结果是:我在后台改了站点标题,前台十分钟内还是旧的。
TTL 这种按时间过期的策略,本质上是在赌「数据多久会变一次」。赌对了没事,赌错了就是用户看到旧数据,而你还以为是自己代码写错了。
后来我把配置这类数据的缓存换成了按文件修改时间失效:
def load_settings_cached(self):
mtime = self.path.stat().st_mtime
if self._cache_mtime == mtime and self._cache is not None:
return self._cache
data = json.loads(self.path.read_text(encoding="utf-8"))
self._cache_mtime = mtime
self._cache = data
return data文件没改就不重新读,改了立刻失效。这比 TTL 准得多,因为它跟的是数据的真实变化,不是时间。
搜索索引也是同一套逻辑。索引重建一次要遍历全部 53 篇文章分词,几十毫秒,不算重,但没必要每次搜都重建。我按 posts.json 的 mtime 判断要不要重建。
什么时候才该上 Redis
我现在的判断标准是三句话。进程只有一个,用字典。进程有多个,就需要一个大家都能看见的地方,这时候 Redis 才有意义。数据大到内存装不下,或者需要跨机器共享,那更得上。
我这三条一条都不占,所以不上。
如果哪天后端开 --workers 4,第一个要挪出去的就是缓存和会话。现在会话存在 sessions.json 里,多进程下同样会打架。
那三天我到底损失了什么
说出来有点丢人:什么都没损失,因为那个首页更新除了我自己没人看。
但这件事让我改掉了两处习惯。一是部署流程里所有「靠记得」的步骤,全部写进脚本。二是看到「改完不生效」的问题,先分清楚是浏览器缓存、nginx 缓存还是应用缓存,这三层的排查命令完全不一样,混在一起查能查一整天。
浏览器层用无痕窗口和 curl 就能排除,nginx 层看响应头的 Cache-Control 和 Last-Modified,应用层就看是不是同一个进程里缓着。我现在出问题会按这个顺序走一遍,一般两三分钟能定位到是哪一层。