开始前,会这些就够了
- 知道浏览器会向服务器发送请求
遇到不熟悉的词,先看这 6 个解释
- 缓存
- 把已有数据或计算结果保留一份,后续满足条件时直接复用,减少重复读取或计算;这份副本何时更新、何时失效,需要另外制定规则。
- TTL
- 在缓存语境里,指一条数据允许保留或被当作有效副本的时间长度。到期后怎样失效、清理或重新读取,要看具体系统的过期规则和使用方式。
- 缓存穿透
- 大量请求查询本来就不存在的数据,缓存又没有保留不存在的结果,于是一次次继续查询后面的数据源;缓存没有替数据库挡住这类重复工作。
- 缓存击穿
- 一个被集中访问的热点数据失去有效缓存时,许多请求同时去读取或重建它,突然把压力转给后面的服务;重点是热点与并发重建叠在一起。
- 缓存雪崩
- 大量缓存同时失去作用后,本来被缓存接住的请求集中转到后端,可能让数据库和依赖服务接连过载;常见诱因是许多条目在相近时间到期。
- 负载均衡
- 在多个可处理请求的后端之间,按轮流、权重或其他规则分配流量,减少请求集中到单个实例的情况;分发依据和健康检查决定实际送给谁。
假设你在练习商城里把一杯茶从十二元改成十五元,后台数据库已是新价格,商品页却一直显示十二元。你刷新、换浏览器,甚至重启前端,价格还是不动。这里没有真实事故或性能测量,我们只用这个场景练习沿路找证据。
“有缓存”只是起点。浏览器可能保存整个响应,CDN(替网站就近分发内容的服务)可能替很多用户保存响应,应用又可能把商品对象放进常用作缓存的 Redis 数据库,或保存在自己的内存。刷新通常只是在这条路上再走一遍,并没有逐层执行删除。
改了数据,缓存会马上知道吗?
先“更新数据源”,再“读取一次”。比较两份版本,接着前进时间或主动清除缓存。
- 模拟时间
- 0 秒
- 数据源版本
- 1
- 缓存保存版本
- 1
- 缓存状态
- 有效
- 0 秒:缓存保存商品版本 1,有效期 20 秒。
这是单实例、无并发、固定 20 秒有效期的旁路缓存模型。时间由按钮推进,不是系统时钟;它不模拟 HTTP 缓存、删除失败、复制延迟或读写竞争。
先问:这次请求走到了哪里?
先打开浏览器网络面板,看请求是否真的发出、返回的正文是什么,再对照服务器的请求日志。如果正文已经是十五元,页面仍画出十二元,就应检查页面状态或渲染逻辑;如果正文也是十二元,继续沿服务端读取路径查,别急着修改样式。
HTTP 的 Age 可以提供缓存响应年龄的线索,但没有 Age 不代表一定访问了源站。应用日志里的商品版本、缓存是否命中、数据库读取结果,能补上 HTTP 头看不到的部分。排查时记录同一次请求的标识,避免拿不同请求的证据拼成一条并不存在的路径。
数据库变了,缓存不会自动知道
下面的小模型有数据库和应用缓存两份值。第一次读取把数据库值放进缓存;之后修改数据库,再读取仍会得到旧值。显式让缓存失效后,下一次读取才重新装入。这就是旁路缓存的基本动作:应用负责查缓存、缺失时查数据源,并负责更新后的失效。
这个程序是单线程、没有过期时间的学习模型,用来定位“改了哪一份”。它没有模拟并发写入、网络失败、多个应用实例,也不能证明线上用先写数据库再删缓存就绝对不会出现旧值。真实系统还要处理删除失败和并发读取回填旧值等情况。
db = {"tea": 12}
cache = {}
def read_price():
if "tea" not in cache:
cache["tea"] = db["tea"]
return cache["tea"]
print(read_price())
db["tea"] = 15
print(read_price())
cache.pop("tea", None)
print(read_price())
怎样运行:Python 3.10+;保存为 cache_demo.py,运行 python cache_demo.py。
运行后对照结果
12
12
15
刷新规则和业务失效规则要分开
HTTP 响应里的 no-cache 允许保存响应,但再次使用前需要成功验证;no-store 则要求不存储这次响应。它们都不是“删除 Redis 所有商品”的指令。即便源站收到一次验证请求,应用仍可能从自己的旧缓存生成结果,所以不同层要各自说明更新规则。
给缓存设置有效期,可以限制某些旧值继续使用的时间,但要看是否续期、是否允许返回过期值、读取的数据源是否本身滞后。不要把“设置了六十秒”直接写成整个系统最多旧六十秒的保证。这里需要的是明确的起算点和完整的数据路径。
只动已经找出的那一层
假设证据表明应用总是命中旧商品,而数据库新值正确,就检查该商品的缓存键和更新流程。别用“清空所有缓存”代替诊断:大量请求同时失去缓存,可能一起压向数据库,也会抹掉你原本要观察的证据。
修好后重复记录更新前、更新后、失效后的同一商品版本。再换一个应用实例读取,确认没有遗漏本地副本。页面显示正确只是一次观察;要说明更新是否可靠,还得观察失败和并发时会怎样。
记住这三点
- 先比较网络响应与页面显示,再决定从哪一层查。
- 数据库、HTTP 缓存和应用缓存各有更新规则。
- 有效期能否限制陈旧时间,要结合续期和读取路径判断。
别把例子用过头
- 单线程字典实验没有覆盖分布式一致性,不能直接当作线上缓存方案。
- no-cache 不等于禁止存储,也不会替应用删除 Redis 数据。
自己试一小步
如果应用有两个独立的本地缓存,删除其中一份后,为什么价格可能来回变化?
- 把实验中的 cache 改成两个字典,让读取交替使用它们。先保持数据库价格为 12,分别通过两个缓存各读取一次,使两份缓存都保存旧价格。
- 再把数据库价格改为 15,只清除其中一份缓存。交替通过两个缓存连续读取四次,记录每次选用的缓存名称与返回值。
- 提出一种让两份缓存都能发现更新的办法,列出它失败时的表现。
做到这些,就算完成
- 能指出每一次返回值来自哪一份数据。
- 没有把一次读取正确当成所有实例均已更新。
- 说明方案允许旧值出现多久,哪些情况仍不能保证。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- RFC 9111:HTTP Caching (新标签页)↗
第 4、5.1、5.2、6 节支持缓存复用、Age、no-cache
/no-store与应用缓存范围。 - Cache-Aside 模式 (新标签页)↗
支持旁路读取、更新后失效、本地副本与不能保证数据源和缓存一致的限制。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
浏览器网络面板:查清哪个请求慢了或失败了 (新标签页)↗
只练网络面板中的 Headers 和 Response,记录同一资源刷新前后的状态码与缓存信息,先判断旧内容是否来自浏览器这一层。
查看学习准备与访问说明 →Redis 官方:缓存淘汰与命中率诊断 (新标签页)↗
只读过期时间与内存淘汰的区别,回到商品缓存例子说明“到期删除”和“内存不够时删掉”为什么不是同一件事。
查看学习准备与访问说明 →