在高并发互联网系统(比如电商秒杀、海量用户查询场景)中,数据库往往是整个链路性能最薄弱的瓶颈。为了分担数据库压力、缩短接口响应耗时,我们会搭建多层级缓存架构,让请求尽可能在离用户更近的层级直接命中返回。
多级缓存系统
所谓多级缓存系统,指在一个系统不同架构层级部署数据缓存,通过就近访问、减少穿透的思路降低下游服务压力,最终整体提升接口访问响应效率、扛住高并发流量。
下面先完整梳理一次用户请求从发出到落地数据库,依次经过的七层缓存链路:
当多层缓存架构搭建完成后,核心的难点就落在缓存与数据库的数据一致性上。业界基于 Redis 衍生了多种成熟的缓存读写策略,其中最经典、落地最广泛的就是旁路缓存策略(Cache Aside Pattern)。
一次完整的用户数据请求,自上而下依次穿透如下缓存链路,每一层都可以拦截请求、直接返回数据,避免直达底层数据库: 用户请求 → 客户端缓存 → CDN 缓存 → 反向代理缓存 → 远程分布式缓存 → 应用程序本地缓存 → 数据库内置缓存 → 磁盘
客户端缓存
客户端缓存运行在用户浏览器 / APP 本地,是离用户最近的一层缓存,开销最低,主要分为两大块:
HTTP Cache(HTTP 协议缓存) 依靠请求头
Cache-Control、Expires、ETag、Last-Modified实现,由服务端控制缓存有效期,缓存静态资源(图片、JS、CSS、HTML),分为强缓存和协商缓存。- 强缓存:资源未过期时,浏览器直接读取本地缓存,不发起任何网络请求;过期后才向服务端发起请求。
Expires(HTTP1.0)、Cache-Control: max-age(HTTP1.1,优先级更高) - 协商缓存:强缓存过期后,浏览器携带资源标识向后端校验,后端比对一致返回
304 Not Modified,浏览器复用本地缓存;数据变更返回 200 + 新数据。生效头:Last-Modified/If-Modified-Since、ETag/If-None-Match
- 强缓存:资源未过期时,浏览器直接读取本地缓存,不发起任何网络请求;过期后才向服务端发起请求。
浏览器本地存储(Web Storage) 不属于 HTTP 协议缓存体系,完全由前端代码读写,后端仅参与 Cookie 下发
- Cookie
- 下发方式:后端响应头
Set-Cookie: key=value; max-age=3600; Path=/; HttpOnly - 特点:每次 HTTP 请求自动携带到请求头;容量小 4KB;可设置过期时间、作用域、安全属性
- 下发方式:后端响应头
- localStorage
- 永久持久化存储,浏览器关闭不丢失,需 JS 手动删除;容量约 5MB;仅字符串存储
- sessionStorage
- 会话级存储,当前标签页关闭自动销毁;隔离不同标签页;容量 5MB
- IndexedDB
- 浏览器数据库,大容量、异步存储,可存对象、二进制数据,适合大量离线数据缓存
- Cookie
HTTP 1.0
Expires
- 格式:
Expires: Wed, 31 Dec 2026 23:59:59 GMT - 作用:设置缓存绝对过期时间,到达该时间强缓存失效
- 缺点:依赖服务器与客户端系统时间,时区、时间不同步会导致缓存错乱,已被 Cache-Control 替代
Last-Modified
- 格式:
Last-Modified: Tue, 11 Aug 2026 02:00:00 GMT - 后端用途:返回资源最后修改的 GMT 时间戳
- 配套请求头:浏览器自动携带
If-Modified-Since: 时间戳 - 后端逻辑:比对请求头时间与文件 / 数据修改时间,一致返回 304
- 缺陷:精度只有秒级,1 秒内多次修改无法识别;只能判断文件修改时间,无法判断内容是否真变化
HTTP1.1
ETag
- 格式:
Last-Modified: Tue, 11 Aug 2026 02:00:00 GMT - 后端用途:返回资源最后修改的 GMT 时间戳
- 配套请求头:浏览器自动携带
If-Modified-Since: 时间戳 - 后端逻辑:比对请求头时间与文件 / 数据修改时间,一致返回 304
- 缺陷:精度只有秒级,1 秒内多次修改无法识别;只能判断文件修改时间,无法判断内容是否真变化
Cache-Control
| 指令 | 核心含义 | 适用场景 |
|---|---|---|
| max-age=N | 设置缓存有效时长,有效期内直接读取本地资源 | JS、CSS、图片等静态资源 |
| public | 允许浏览器、CDN、反向代理多层级缓存 | 公开静态资源、CDN 分发文件 |
| private | 仅限当前用户浏览器缓存,代理服务器不可缓存 | 含隐私的个性化动态页面 |
| s-maxage=N | 仅对 CDN、Nginx 代理生效,浏览器不识别 | 单独配置边缘节点缓存周期 |
| must-revalidate | 缓存过期强制校验,杜绝读取过期脏数据 | 系统配置文件、核心静态资源 |
| no-cache | 本地保留缓存,每次请求都向服务端校验更新 | 需实时同步的动态接口、商品页面 |
| no-store | 完全不做任何本地缓存,每次请求拉取最新数据 | 登录、支付等敏感页面 |
常用组合示例
Redis中常用的缓存读写策略
1. Cache Aside Pattern (旁路缓存策略)
旁路缓存策略是我们日常开发中最常用、 最经典的一种“缓存 + 数据库”读写模式,分为 读策略和 写策略,属于最终一致性。
- 读策略: 在读取时先读取缓存,如果命中,则直接返回;如果为命中,则访问数据库,然后回填到缓存中。
- 写策略: 应用先更新数据库,然后删除缓存中对应的数据。
为什么是先更新数据库,后删除缓存?
CodeBlock Loading...结果:DB中是新的值,而Cache中是旧值,不能保证先到的请求一定先执行完所有变更逻辑 ,导致数据不一致
先更新数据库再删缓存一定安全吗? 时许分析(请求A读,请求B写)
CodeBlock Loading...结果:DB中是新值,缓存中又变成了旧值,所以旁路缓存策略 一般需要搭配延迟双删和TTL使用,确保缓存值正确
为什么是“删除Cache”,而不是“更新Cache”?
- 性能开销:写操作往往只更新了对象的部分字段,如果为了“更新Cache”而去重新查询或计算整个缓存对象 ,开销可能很大。相比之下,“删除”是一个轻量级 的操作。
- 懒加载思想:”删除“操作遵循懒加载原则。只有当 数据下一次被真正需要(被读取)时,才从数据库 加载并写入缓存,避免了无效的缓存更新。
- 并发安全:”更新缓存“再高并发下也有可能出现向上面的更新顺序错乱的问题从而导致脏数据
1.1 延迟双删
延时双删(Delayed Double Deletion)是一种缓存一致性策略,用于解决在高并发环境下,数据库与缓存数据可能不一致的问题。具体来说,延时双删通常在以下场景中使:
场景描述
在系统中,数据可能同时存在于缓存和数据库中。当某个数据发生更新操作时,为了保证缓存中的数据与数据库中的数据一致,通常会在更新数据库后立即删除缓存中的旧数据。然而,在高并发场景下,可能会出现以下问题:
- 数据更新与缓存读操作并发:
- 假设在某个请求 A 进行数据更新时,更新了数据库并删除了缓存中的旧数据。
- 但是,在删除缓存之后,数据库事务还未提交成功,此时可能会有另一个请求 B 读取数据。
- 如果请求 B 先于事务提交,B 会发现缓存为空,然后查询数据库,并将旧数据重新写入缓存,导致缓存与数据库中的数据不一致。
延时双删策略
为了避免这种数据不一致的情况,可以使用延时双删策略。其步骤如下:
- 第一次删除缓存:
- 在数据更新操作之前先删除缓存中的旧数据。
- 更新数据库:
- 进行数据库更新操作,并提交事务。
- 延时一定时间后再次删除缓存:
- 在数据库更新完成并提交事务后,设置一个短暂的延迟(例如 500 毫秒或 1 秒),然后再次删除缓存。这是为了防止在事务提交后但缓存还未更新的时间窗口中,产生不一致的情况。
2. Read/Write Through Pattern(读写穿透)
应用程序只操作缓存,完全不直接碰数据库,由缓存中间件 / 封装的 SDK 内部同步操作 DB,所有的读写请求都直接打向 Cache,保证缓存和数据库强一致。
Read Through(读穿透)
- 应用从Cache读取数据
- 如果命中,直接返回
如果未命中,由Cache服务自己负责从DB加载数据,加载成功后先写入自身 ,再返回应用。
WriteThrough(写穿透)
- 先查Cache,Cache中不存在,直接更新数据库
- Cache中存在,则先更新Cache,然后Cache服务自己更新数据库。只有当Cache和DB都写入成功时,才向上层返回成功
3. Write Behind Pattern(异步缓存写入)
Write Behind(也常被称为 Write-Back) Pattern 和 Read/Write Through Pattern 很相似,两者都是由 Cache 服务来负责 Cache 和 数据库 的读写,但是 Write Behind 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 数据库。
Write Behind(写操作)
- 应用将数据写入Cache,然后立即返回。
- Cache服务将这个写操作放入一个队列中 。
- 通过 一个独立的异步线程 / 任务,将队列中的写操作批量地、合并地写入数据库 这种模式对数据一致性带来了挑战(例如:Cache 中的数据还没来得及写回 DB,系统就宕机了),因此不适用于需要强一致性的场景(如交易、库存)。
但是,它的异步和批量特性,带来了无与伦比的写性能。它在很多高性能系统中都有广泛应用:
- MySQL 的 InnoDB Buffer Pool 机制: 数据修改先在内存 Buffer Pool 中完成,然后由后台线程异步刷写到磁盘。
- 操作系统的页缓存(Page Cache): 文件写入也是先写到内存,再由操作系统异步刷盘。
- 高频计数场景: 对于文章浏览量、帖子点赞数这类允许短暂数据不一致、但写入极其频繁的场景,可以先在 Redis 中快速累加,再通过定时任务异步同步回数据库。