7c70c37072
Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation. Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
682 lines
31 KiB
Markdown
682 lines
31 KiB
Markdown
# 缓存系统设计:从淘宝商品详情页看高性能架构
|
||
|
||
> 💡 **学习指南**:为什么用户点击淘宝商品详情页只需要 50 毫秒,而直接查数据库要 500 毫秒?这背后是一个精心设计的缓存架构在发挥作用。本章节将带你深入理解缓存的本质、模式与实战技巧。
|
||
|
||
---
|
||
|
||
## 0. 引言:为什么系统越来越慢?
|
||
|
||
### 0.1 一个真实的性能危机
|
||
|
||
2020 年双十一,某电商平台的数据库 CPU 使用率突然飙到 95%,订单查询响应时间从 100ms 暴涨到 8 秒。问题根源很快查明:一个新上线的促销页面,每次加载都要查询 50+ 次数据库,且没有缓存。
|
||
|
||
**核心问题暴露**——当流量激增时,数据库成为整个系统的瓶颈。
|
||
|
||
| 操作类型 | 响应时间 | 单节点 QPS | 瓶颈分析 |
|
||
| :------- | :------- | :--------- | :------- |
|
||
| L1 Cache 读取 | ~0.5 ns | 数十亿级 | CPU 内置,无瓶颈 |
|
||
| 内存读取 | ~100 ns | 百万级 | 内存带宽 |
|
||
| Redis 查询 | ~1 ms | 10万级 | 网络延迟 |
|
||
| MySQL 查询 | ~10 ms | 数千级 | 磁盘 IO |
|
||
| 跨机房查询 | ~100 ms | 百级 | 网络距离 |
|
||
|
||
**性能差距触目惊心**:内存操作比 MySQL 查询快 100,000 倍。
|
||
|
||
### 0.2 缓存的本质定义
|
||
|
||
缓存不是简单地把数据存到内存,而是一种**基于局部性原理的数据访问优化技术**。
|
||
|
||
**严格定义**:缓存是位于计算单元与慢速存储之间的高速数据存储层,通过存储最近或频繁访问数据的副本,减少访问延迟、提升系统吞吐量。
|
||
|
||
**与原始存储的核心区别**:
|
||
- 原始存储(数据库):容量大、持久化、查询慢
|
||
- 缓存:容量有限、易失性、查询极快
|
||
- 缓存内容永远是原始数据的**副本**,而非主数据
|
||
|
||
### 0.3 淘宝商品详情页的缓存实战
|
||
|
||
让我们拆解一个真实的电商商品详情页,看看缓存是如何层层发挥作用的。
|
||
|
||
**场景**:用户打开商品 ID 为 12345 的详情页。
|
||
|
||
**访问链路**(从上到下,层层穿透):
|
||
|
||
| 层级 | 存储位置 | 查询耗时 | 数据示例 | 命中率 |
|
||
| :--- | :------- | :------- | :------- | :----- |
|
||
| L1 | 浏览器本地缓存 | ~0ms | 静态图片、CSS、JS | 95%+ |
|
||
| L2 | CDN 边缘节点 | ~20ms | 商品主图、详情图 | 90%+ |
|
||
| L3 | Nginx 本地缓存 | ~1ms | 商品基础信息 JSON | 80%+ |
|
||
| L4 | 应用进程本地缓存 (Caffeine) | ~0.1ms | 热销商品详情 | 90%+ |
|
||
| L5 | Redis 集群 | ~2ms | 所有商品信息、库存 | 99%+ |
|
||
| L6 | MySQL 主从集群 | ~10ms | 全量商品数据 | 100% |
|
||
|
||
**最终效果**:
|
||
- 95% 的请求在前 4 层就被拦截,根本不会到达数据库
|
||
- 整体响应时间:P99 < 100ms
|
||
- 数据库 QPS 从理论上的 100万+ 下降到实际 5000 以下
|
||
|
||
<CacheArchitectureOverview />
|
||
|
||
---
|
||
|
||
## 1. 缓存的底层原理:局部性
|
||
|
||
缓存之所以有效,根植于计算机科学中一个被反复验证的观察:**局部性原理**。
|
||
|
||
### 1.1 时间局部性 (Temporal Locality)
|
||
|
||
**定义**:如果一个数据项被访问,那么在不久的将来它很可能再次被访问。
|
||
|
||
**技术解释**:程序在执行过程中,往往会对某些变量或数据对象进行反复读取或修改。这是因为:
|
||
- 循环结构会重复访问相同的计数器变量
|
||
- 函数调用会重复访问相同的参数和局部变量
|
||
- 热点数据(如用户会话、配置项)会被频繁查询
|
||
|
||
**实例**:用户登录后,系统需要反复查询该用户的权限信息。第一次查询后将结果缓存,后续几十次请求都可以直接从缓存获取,直到用户登出或权限变更。
|
||
|
||
### 1.2 空间局部性 (Spatial Locality)
|
||
|
||
**定义**:如果一个数据项被访问,那么与它地址相邻的数据项也很可能被访问。
|
||
|
||
**技术解释**:计算机存储和访问数据的方式天然具有连续性:
|
||
- 内存以缓存行(通常 64 字节)为单位加载数据
|
||
- 数组、列表等数据结构在内存中连续存储
|
||
- 磁盘读取以块(通常 4KB)为单位
|
||
- 业务数据往往按时间、分类等维度聚簇存储
|
||
|
||
**实例**:加载商品列表时,如果缓存了第 1-10 条商品数据,当用户翻页到第 11-20 条时,这些相邻的数据很可能已经被批量加载到缓存中,实现快速响应。
|
||
|
||
### 1.3 缓存的生命周期
|
||
|
||
一个缓存条目从创建到销毁,经历完整的生命周期:
|
||
|
||
```
|
||
写入 (Write) → 命中/未命中 (Hit/Miss) → 过期 (Expiration) → 淘汰 (Eviction)
|
||
```
|
||
|
||
**写入策略**:
|
||
- **主动写入**:系统启动时预加载热点数据(缓存预热)
|
||
- **懒加载**:首次访问时从数据库加载并写入缓存
|
||
- **异步更新**:后台线程定期刷新即将过期的数据
|
||
|
||
**过期策略**:
|
||
- **TTL (Time To Live)**:设置固定生存时间,到期自动失效
|
||
- **滑动过期**:每次访问后重置过期时间
|
||
- **绝对过期**:指定具体过期时间点
|
||
|
||
**淘汰策略**(缓存满时):
|
||
- **LRU (Least Recently Used)**:淘汰最近最少使用(最常用)
|
||
- **LFU (Least Frequently Used)**:淘汰访问频率最低
|
||
- **FIFO (First In First Out)**:先进先出
|
||
- **Random**:随机淘汰
|
||
|
||
---
|
||
|
||
## 2. 缓存架构选型
|
||
|
||
### 2.1 本地缓存 (Local Cache)
|
||
|
||
**定义**:与应用进程共享内存空间的缓存,无需网络访问。
|
||
|
||
**技术特点**:
|
||
- **访问延迟**:~100 纳秒(纯内存访问,无网络开销)
|
||
- **容量限制**:受限于单机内存(通常几百 MB 到几 GB)
|
||
- **一致性挑战**:多实例部署时,各节点缓存独立,数据可能不一致
|
||
- **进程绑定**:应用重启,缓存丢失
|
||
|
||
**主流实现**:
|
||
|
||
| 语言 | 库/框架 | 核心特性 |
|
||
| :--- | :------ | :------- |
|
||
| Java | Caffeine | 高性能,W-TinyLFU 淘汰算法,近乎完美的命中率 |
|
||
| Java | Guava Cache | 功能全面,但性能略逊于 Caffeine |
|
||
| Go | Ristretto | 高性能,高命中率,支持过期 |
|
||
| Go | BigCache | 专为大量 entry 设计,零 GC 压力 |
|
||
| Python | cachetools | 多种缓存策略,TTL 支持 |
|
||
|
||
### 2.2 分布式缓存 (Distributed Cache)
|
||
|
||
**定义**:作为独立服务部署的缓存系统,通过网络协议访问,多实例共享。
|
||
|
||
**技术特点**:
|
||
- **访问延迟**:~1-5 毫秒(网络开销)
|
||
- **容量扩展**:支持集群部署,容量可达数百 GB 甚至 TB
|
||
- **一致性保证**:所有应用实例访问同一份数据,天然一致
|
||
- **高可用**:支持主从复制、哨兵、集群模式
|
||
|
||
**主流实现对比**:
|
||
|
||
| 特性 | Redis | Memcached |
|
||
| :--- | :---- | :---------- |
|
||
| 数据结构 | String, Hash, List, Set, ZSet, Stream 等 | 仅 String |
|
||
| 持久化 | RDB, AOF 支持 | 不支持 |
|
||
| 集群 | Redis Cluster, Codis | 客户端分片 |
|
||
| 单线程/多线程 | 单线程(命令执行)| 多线程 |
|
||
| 内存管理 | 支持内存淘汰策略 | LRU 淘汰 |
|
||
| 适用场景 | 复杂数据结构、持久化需求 | 纯缓存、简单 KV |
|
||
|
||
### 2.3 多级缓存架构
|
||
|
||
在真实的生产环境中,单一缓存层往往无法满足性能和成本的双重需求。多级缓存架构通过在不同层级部署缓存,形成层层防护,最大化性能收益。
|
||
|
||
**典型多级缓存架构**:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 用户请求 │
|
||
└─────────────────────────┬───────────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ L1: 浏览器缓存 (Browser Cache) │
|
||
│ - 存储:静态资源 (CSS/JS/图片) │
|
||
│ - 控制:Cache-Control, ETag, Last-Modified │
|
||
│ - 延迟:~0ms (本地磁盘/内存) │
|
||
└─────────────────────────┬───────────────────────────────────────┘
|
||
↓ (未命中)
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ L2: CDN 缓存 (Content Delivery Network) │
|
||
│ - 存储:静态资源、部分 API 响应 │
|
||
│ - 节点:全球分布,就近访问 │
|
||
│ - 延迟:~20-50ms │
|
||
└─────────────────────────┬───────────────────────────────────────┘
|
||
↓ (未命中)
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ L3: 反向代理缓存 (Nginx/Varnish) │
|
||
│ - 存储:完整 HTTP 响应、聚合数据 │
|
||
│ - 延迟:~1-5ms │
|
||
└─────────────────────────┬───────────────────────────────────────┘
|
||
↓ (未命中)
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ L4: 应用本地缓存 (Caffeine/Guava) │
|
||
│ - 存储:热点对象、配置、用户会话 │
|
||
│ - 延迟:~0.1ms │
|
||
└─────────────────────────┬───────────────────────────────────────┘
|
||
↓ (未命中)
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ L5: 分布式缓存 (Redis Cluster) │
|
||
│ - 存储:业务数据、会话、计算结果 │
|
||
│ - 延迟:~1-5ms │
|
||
└─────────────────────────┬───────────────────────────────────────┘
|
||
↓ (未命中)
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ L6: 数据库 (MySQL/PostgreSQL) │
|
||
│ - 存储:全量数据 │
|
||
│ - 延迟:~10-50ms │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
<CacheHierarchyDemo />
|
||
|
||
---
|
||
|
||
## 3. 缓存设计模式
|
||
|
||
当引入缓存层后,应用程序需要确定如何与缓存和数据库交互。业界形成了四种经典的设计模式,每种模式在一致性、性能和复杂度之间做出不同的权衡。
|
||
|
||
### 3.1 Cache-Aside (旁路缓存) — 最常用
|
||
|
||
**模式定义**:应用程序负责直接管理缓存,显式地从缓存读取、写入数据。缓存对数据库完全透明,不知道数据库的存在。
|
||
|
||
**读取流程**:
|
||
```
|
||
应用程序 ─┬─→ 查询缓存 ──→ 命中?─┬─是→ 返回数据
|
||
│ │
|
||
│ └─否→ 查询数据库
|
||
│ ↓
|
||
│ 写入缓存
|
||
│ ↓
|
||
└──────────────────────── 返回数据
|
||
```
|
||
|
||
**更新流程**(关键!):
|
||
```
|
||
应用程序 ──→ 更新数据库 ──→ 删除缓存(不是更新缓存!)
|
||
```
|
||
|
||
### 3.2 Read-Through (读穿透)
|
||
|
||
**模式定义**:应用程序只与缓存交互,缓存层负责在缺失时自动从数据库加载数据。对应用程序完全透明。
|
||
|
||
**工作原理**:
|
||
```
|
||
应用程序 ──→ 查询缓存 ──→ 未命中 ──→ 缓存服务自动加载数据库数据
|
||
↓
|
||
返回给应用
|
||
```
|
||
|
||
**优缺点对比**:
|
||
|
||
| 维度 | Read-Through | Cache-Aside |
|
||
| :--- | :----------- | :------------ |
|
||
| 代码复杂度 | 低(业务代码无缓存逻辑) | 中(显式管理缓存) |
|
||
| 灵活性 | 低(受限于缓存库实现) | 高(完全控制) |
|
||
| 首次加载延迟 | 较高(穿透到库) | 可控(可预热) |
|
||
| 适用场景 | 标准化缓存需求 | 复杂业务逻辑 |
|
||
|
||
### 3.3 Write-Through (写穿透)
|
||
|
||
**模式定义**:应用程序写入缓存,缓存服务同步将数据写入数据库,确保缓存和数据库强一致。
|
||
|
||
**工作原理**:
|
||
```
|
||
应用程序 ──→ 写入缓存 ──→ 缓存服务同步写入数据库 ──→ 返回成功
|
||
```
|
||
|
||
**特点**:
|
||
- **强一致性**:缓存和数据库始终一致
|
||
- **写入延迟高**:需等待数据库写入完成
|
||
- **吞吐量受限**:受限于数据库写入能力
|
||
|
||
**适用场景**:
|
||
- 对数据一致性要求极高的场景(金融交易、库存扣减)
|
||
- 写操作相对较少的场景
|
||
|
||
<CachePatternComparisonDemo />
|
||
|
||
### 3.4 Write-Behind (异步写回)
|
||
|
||
**模式定义**:应用程序只写入缓存,缓存服务异步批量将数据写入数据库。
|
||
|
||
**工作原理**:
|
||
```
|
||
应用程序 ──→ 写入缓存(立即返回)
|
||
↓
|
||
后台异步批量写入数据库
|
||
```
|
||
|
||
**核心优势**:
|
||
- **写入极快**:~1ms 延迟,10万+ QPS
|
||
- **批量写入**:减少数据库 IO,提升吞吐量
|
||
- **削峰填谷**:将突发流量平滑化
|
||
|
||
**核心风险**:
|
||
- **数据丢失风险**:缓存宕机,未写入数据库的数据丢失
|
||
- **数据不一致窗口**:异步写入期间,缓存与数据库不一致
|
||
|
||
---
|
||
|
||
## 4. 缓存的三大经典问题
|
||
|
||
在实际生产环境中,缓存可能引入三类严重问题,需要系统性解决方案。
|
||
|
||
### 4.1 缓存穿透 (Cache Penetration)
|
||
|
||
**问题定义**:查询一个**不存在的数据**,缓存中没有(因为没有),数据库中也没有,导致每次请求都直接打到数据库。
|
||
|
||
**攻击场景**:
|
||
- 恶意攻击者构造大量不存在的 ID 进行查询(如 id=-1, id=999999999)
|
||
- 爬虫遍历不存在的资源路径
|
||
- 业务逻辑错误导致查询无效数据
|
||
|
||
**解决方案 1:布隆过滤器 (Bloom Filter)**
|
||
|
||
**原理**:在缓存之前加一层概率型数据结构,快速判断"这个 key **肯定不存在**或**可能存在**"。
|
||
|
||
**特性**:
|
||
- 100% 判断不存在(绝对不会误判为存在)
|
||
- 可能误判存在(实际不存在,但过滤器说可能存在,概率可调)
|
||
|
||
### 4.2 缓存击穿 (Cache Breakdown)
|
||
|
||
**问题定义**:某个**热点数据**在缓存中过期(TTL 到期),此时大量并发请求同时到达,都去查询数据库,导致数据库压力骤增。
|
||
|
||
**典型场景**:
|
||
- 微博热搜榜过期瞬间
|
||
- 明星八卦新闻缓存失效
|
||
- 秒杀活动开始时的库存数据
|
||
- 热门商品的缓存同时过期
|
||
|
||
**解决方案 1:互斥锁 (Mutex Lock)**
|
||
|
||
**原理**:当缓存失效时,只允许一个线程去查询数据库并重建缓存,其他线程等待或重试。
|
||
|
||
**解决方案 2:逻辑过期 (Logical Expiration)**
|
||
|
||
**原理**:不设置物理过期时间(TTL),而是在缓存 value 中嵌入逻辑过期时间字段。发现逻辑过期时,异步重建缓存,同时返回旧数据(永不过期)。
|
||
|
||
### 4.3 缓存雪崩 (Cache Avalanche)
|
||
|
||
**问题定义**:大量缓存数据在**同一时间点集中过期**(或 Redis 宕机),导致所有请求同时穿透到数据库,瞬间压垮数据库。
|
||
|
||
**典型触发场景**:
|
||
- 系统重启后,所有缓存从 0 开始重建,同时设置相同 TTL
|
||
- 定时任务批量刷新缓存,设置相同的过期时间
|
||
- 缓存服务(Redis)宕机或网络分区
|
||
- 大量热点数据同时达到过期时间
|
||
|
||
**解决方案 1:随机 TTL (Time To Live)**
|
||
|
||
**原理**:在基础过期时间上增加随机偏移量,打散过期时间点。
|
||
|
||
**解决方案 2:缓存预热 (Cache Preheating)**
|
||
|
||
**原理**:系统启动或定时任务主动将热点数据加载到缓存,避免冷启动时集中回源。
|
||
|
||
**解决方案 3:熔断降级 (Circuit Breaker)**
|
||
|
||
**原理**:当数据库压力过大时,暂时拒绝部分请求或返回降级数据,保护数据库不被压垮。
|
||
|
||
---
|
||
|
||
## 5. 缓存一致性策略
|
||
|
||
缓存的本质是数据的副本,副本与主数据(数据库)之间必然存在不一致的时间窗口。如何控制这个时间窗口,是缓存设计的核心挑战。
|
||
|
||
### 5.1 为什么不一致?
|
||
|
||
**并发写入场景**:
|
||
|
||
| 时间 | 线程 A(更新 age=25) | 线程 B(查询) | 数据库 | 缓存 |
|
||
| :--- | :-------------------- | :------------- | :----- | :--- |
|
||
| T1 | 更新 DB age=25 | - | 25 | 20(旧值) |
|
||
| T2 | - | 查询缓存(命中旧值) | 25 | 20 ❌ |
|
||
| T3 | 删除缓存 | - | 25 | - |
|
||
| T4 | - | - | 25 | 从 DB 加载 25 ✅ |
|
||
|
||
**问题**:在 T2 时刻,线程 B 读取到了缓存中的旧值 20,而此时数据库已经是 25。
|
||
|
||
### 5.2 最佳实践:先更新数据库,再删除缓存
|
||
|
||
**原理**:利用数据库的行锁机制,保证"更新"操作的排他性,最大限度减少不一致窗口。
|
||
|
||
**流程**:
|
||
|
||
```
|
||
写请求 ──→ 开启数据库事务 ──→ 更新数据库记录 ──→ 提交事务 ──→ 删除缓存
|
||
```
|
||
|
||
**为什么这个顺序最优?**
|
||
|
||
1. **数据库锁保护**:更新操作会获取行锁,其他读写操作必须等待,天然排他
|
||
2. **删除缓存是异步操作**:不需要等待缓存删除成功,数据库提交后即可返回
|
||
3. **极端情况仍可接受**:即使缓存删除失败,只是下次读取会回源,不会导致脏数据长期存在
|
||
|
||
### 5.3 延迟双删 (Delayed Double Deletion)
|
||
|
||
**场景**:先删缓存、再写 DB 的方案在极端并发下仍有不一致风险。延迟双删通过两次删除,最大限度保证一致性。
|
||
|
||
**流程**:
|
||
|
||
```
|
||
1. 删除缓存
|
||
2. 更新数据库
|
||
3. 等待一段时间(如 500ms)
|
||
4. 再次删除缓存
|
||
```
|
||
|
||
**三种一致性策略对比**:
|
||
|
||
| 策略 | 一致性级别 | 性能影响 | 复杂度 | 适用场景 |
|
||
| :--- | :--------- | :--------- | :------- | :------- |
|
||
| 先更新 DB,再删缓存 | 最终一致 | 低 | 低 | 大多数场景,推荐作为默认方案 |
|
||
| 延迟双删 | 强最终一致 | 中(延迟) | 中 | 对一致性要求较高的场景 |
|
||
| 先删缓存,再更新 DB | 弱 | 低 | 低 | 不推荐,易出现不一致 |
|
||
|
||
---
|
||
|
||
## 6. 实战:构建高性能缓存系统
|
||
|
||
### 6.1 场景分析:电商商品详情页
|
||
|
||
**业务特点**:
|
||
- 读多写少:100:1 的读写比
|
||
- 热点集中:20% 的商品贡献 80% 的流量
|
||
- 数据复杂度:商品基础信息 + 价格 + 库存 + 评价聚合
|
||
- 一致性要求:价格、库存强一致,其他可最终一致
|
||
|
||
**性能指标**:
|
||
- P99 响应时间 < 100ms
|
||
- 数据库 QPS 峰值 < 5000
|
||
- 缓存命中率 > 95%
|
||
|
||
<CacheProblemsDemo />
|
||
|
||
### 6.2 架构设计
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 客户端请求 │
|
||
└───────────────────────┬─────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ CDN 缓存层 │
|
||
│ - 商品主图、详情图 │
|
||
│ - Cache-Control: max-age=86400 │
|
||
└───────────────────────┬─────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Nginx 缓存层 │
|
||
│ - 商品基础信息聚合(包含价格、库存) │
|
||
│ - proxy_cache_valid 5m │
|
||
└───────────────────────┬─────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 应用层 │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ 本地缓存 │ │ 分布式锁 │ │
|
||
│ │ (Caffeine) │ │ (Redisson) │ │
|
||
│ │ - 热点商品 │ │ - 防击穿 │ │
|
||
│ │ - 配置信息 │ │ - 库存扣减 │ │
|
||
│ └─────────────────┘ └─────────────────┘ │
|
||
└───────────────────────┬─────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Redis 集群 │
|
||
│ - 商品详情 Hash │
|
||
│ - 库存计数器 String │
|
||
│ - 热点商品列表 ZSet │
|
||
│ - 分布式锁 │
|
||
└───────────────────────┬─────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ MySQL 集群 │
|
||
│ - 商品主表 │
|
||
│ - 库存表(行锁控制并发) │
|
||
│ - 订单表 │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 6.3 核心代码实现
|
||
|
||
**完整的多级缓存实现(Java + Spring Boot)**:
|
||
|
||
```java
|
||
@Component
|
||
public class MultiLevelCache {
|
||
|
||
// 本地缓存:Caffeine
|
||
private final Cache<String, Object> localCache;
|
||
|
||
@Autowired
|
||
private StringRedisTemplate redisTemplate;
|
||
|
||
@Autowired
|
||
private RedissonClient redissonClient;
|
||
|
||
// 缓存层级配置
|
||
private static final long LOCAL_TTL_SECONDS = 30;
|
||
private static final long REDIS_TTL_SECONDS = 300;
|
||
|
||
public MultiLevelCache() {
|
||
this.localCache = Caffeine.newBuilder()
|
||
.maximumSize(10_000)
|
||
.expireAfterWrite(Duration.ofSeconds(LOCAL_TTL_SECONDS))
|
||
.recordStats()
|
||
.build();
|
||
}
|
||
|
||
/**
|
||
* 多级缓存读取(L1: 本地缓存 -> L2: Redis -> L3: 数据库)
|
||
*/
|
||
public <T> T get(String key, Class<T> type, Supplier<T> dbLoader) {
|
||
// L1: 本地缓存
|
||
Object localValue = localCache.getIfPresent(key);
|
||
if (localValue != null) {
|
||
return type.cast(localValue);
|
||
}
|
||
|
||
// L2: Redis 缓存(带分布式锁防击穿)
|
||
String redisKey = "cache:" + key;
|
||
String redisValue = redisTemplate.opsForValue().get(redisKey);
|
||
|
||
if (StrUtil.isNotBlank(redisValue)) {
|
||
T value = JSON.parseObject(redisValue, type);
|
||
// 回填本地缓存
|
||
localCache.put(key, value);
|
||
return value;
|
||
}
|
||
|
||
// L3: 数据库(带分布式锁)
|
||
String lockKey = "lock:" + key;
|
||
RLock lock = redissonClient.getLock(lockKey);
|
||
|
||
try {
|
||
boolean acquired = lock.tryLock(100, 10, TimeUnit.MILLISECONDS);
|
||
if (!acquired) {
|
||
// 获取锁失败,等待后重试
|
||
Thread.sleep(50);
|
||
return get(key, type, dbLoader);
|
||
}
|
||
|
||
// 双重检查
|
||
String doubleCheck = redisTemplate.opsForValue().get(redisKey);
|
||
if (StrUtil.isNotBlank(doubleCheck)) {
|
||
return JSON.parseObject(doubleCheck, type);
|
||
}
|
||
|
||
// 查询数据库
|
||
T value = dbLoader.get();
|
||
|
||
if (value != null) {
|
||
// 写入 Redis(带随机 TTL)
|
||
long ttl = REDIS_TTL_SECONDS + RandomUtil.randomInt(-30, 30);
|
||
redisTemplate.opsForValue().set(
|
||
redisKey,
|
||
JSON.toJSONString(value),
|
||
ttl,
|
||
TimeUnit.SECONDS
|
||
);
|
||
|
||
// 回填本地缓存
|
||
localCache.put(key, value);
|
||
}
|
||
|
||
return value;
|
||
|
||
} catch (InterruptedException e) {
|
||
Thread.currentThread().interrupt();
|
||
throw new RuntimeException("获取锁被中断", e);
|
||
} finally {
|
||
if (lock.isHeldByCurrentThread()) {
|
||
lock.unlock();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除缓存(更新时调用)
|
||
*/
|
||
public void evict(String key) {
|
||
// 删除本地缓存
|
||
localCache.invalidate(key);
|
||
|
||
// 删除 Redis 缓存
|
||
redisTemplate.delete("cache:" + key);
|
||
}
|
||
}
|
||
```
|
||
|
||
<EcommerceCacheArchitectureDemo />
|
||
|
||
---
|
||
|
||
## 7. 总结与学习路径
|
||
|
||
### 7.1 核心知识点回顾
|
||
|
||
| 知识点 | 关键概念 | 实战要点 |
|
||
| :----- | :------- | :------- |
|
||
| **局部性原理** | 时间局部性、空间局部性 | 根据访问模式设计缓存 key 和预热策略 |
|
||
| **多级缓存** | L1-L6 层级 | 浏览器 → CDN → Nginx → 本地 → Redis → DB |
|
||
| **缓存模式** | Cache-Aside、Read-Through、Write-Through、Write-Behind | Cache-Aside 最常用,Write-Behind 用于高并发写入 |
|
||
| **缓存穿透** | 查询不存在数据 | 布隆过滤器 + 缓存空对象 |
|
||
| **缓存击穿** | 热点数据过期 | 互斥锁 + 逻辑过期 |
|
||
| **缓存雪崩** | 大量数据同时过期 | 随机 TTL + 缓存预热 + 熔断降级 |
|
||
| **一致性策略** | 先更新 DB 再删缓存、延迟双删 | Cache-Aside + 延迟双删应对极端并发 |
|
||
|
||
### 7.2 学习路径建议
|
||
|
||
**阶段 1:理解原理(1-2 天)**
|
||
- 掌握局部性原理(时间、空间)
|
||
- 理解缓存生命周期(写入 → 命中 → 过期 → 淘汰)
|
||
- 了解不同存储介质的性能差异(CPU Cache → 内存 → SSD → HDD)
|
||
|
||
**阶段 2:掌握基础模式(2-3 天)**
|
||
- 实现 Cache-Aside 模式(读取、更新)
|
||
- 使用 Redis 做分布式缓存
|
||
- 理解为什么"更新时删除缓存而不是更新缓存"
|
||
|
||
**阶段 3:多级缓存实战(3-5 天)**
|
||
- 实现本地缓存(Caffeine/Guava)+ Redis 的两级架构
|
||
- 解决多级缓存的一致性问题
|
||
- 实现缓存统计和监控
|
||
|
||
**阶段 4:解决经典问题(1 周)**
|
||
- 缓存穿透:实现布隆过滤器
|
||
- 缓存击穿:实现互斥锁和逻辑过期
|
||
- 缓存雪崩:实现随机 TTL、缓存预热、熔断降级
|
||
|
||
**阶段 5:一致性保障(1-2 周)**
|
||
- 深入理解 Cache-Aside 模式的并发问题
|
||
- 实现延迟双删
|
||
- 了解 Binlog 订阅方案(Canal/Debezium)
|
||
|
||
**阶段 6:生产级实战(持续)**
|
||
- 设计完整的商品详情页缓存系统
|
||
- 搭建 Prometheus + Grafana 监控体系
|
||
- 进行压测验证和性能调优
|
||
|
||
### 7.3 推荐资源
|
||
|
||
**书籍**:
|
||
- 《Redis 设计与实现》(黄健宏)—— 深入理解 Redis 内部机制
|
||
- 《高性能 MySQL》(第 5 章:缓存策略)
|
||
|
||
**文章**:
|
||
- Martin Fowler: *Patterns of Distributed Systems*
|
||
- Redis 官方文档:https://redis.io/docs/
|
||
- Google: *Designing a Cache System*
|
||
|
||
**开源项目**:
|
||
- Caffeine(Java 本地缓存)
|
||
- Redisson(Java Redis 客户端,提供分布式锁等)
|
||
- JetCache(阿里开源的多级缓存框架)
|
||
|
||
<CacheMonitoringDashboardDemo />
|
||
|
||
---
|
||
|
||
## 8. 名词速查表 (Glossary)
|
||
|
||
| 名词 | 全称 | 解释 |
|
||
| :--- | :--- | :--- |
|
||
| **Cache** | - | **缓存**。存储数据副本的快速存储层,用于加速访问。 |
|
||
| **Hit** | - | **缓存命中**。请求的数据在缓存中找到,无需访问数据库。 |
|
||
| **Miss** | - | **缓存未命中**。请求的数据不在缓存中,需要回源查询。 |
|
||
| **Hit Ratio** | - | **命中率**。缓存命中的请求数占总请求数的比例(目标: > 95%)。 |
|
||
| **TTL** | Time To Live | **生存时间**。缓存条目的过期时间。 |
|
||
| **Eviction** | - | **淘汰**。缓存满了时,删除旧数据为新数据腾空间。 |
|
||
| **LRU** | Least Recently Used | **最近最少使用**。常见的缓存淘汰策略。 |
|
||
| **LFU** | Least Frequently Used | **最不经常使用**。按访问频率淘汰的策略。 |
|
||
| **Cache Penetration** | - | **缓存穿透**。查询不存在数据,导致请求直接打到数据库。 |
|
||
| **Cache Breakdown** | - | **缓存击穿**。热点数据过期,瞬间大量请求打到数据库。 |
|
||
| **Cache Avalanche** | - | **缓存雪崩**。大量缓存同时过期,数据库压力骤增。 |
|
||
| **Bloom Filter** | - | **布隆过滤器**。空间效率高的概率型数据结构,用于判断元素是否可能存在。 |
|
||
| **Cache-Aside** | - | **旁路缓存**。应用代码直接操作缓存和数据库的模式。 |
|
||
| **Read-Through** | - | **读穿透**。缓存库自动从数据库加载数据。 |
|
||
| **Write-Through** | - | **写穿透**。写入缓存时同步写入数据库。 |
|
||
| **Write-Behind** | - | **异步写回**。写入缓存后异步批量写数据库。 |
|
||
| **Local Cache** | - | **本地缓存**。与应用在同一进程内的缓存(如 Caffeine)。 |
|
||
| **Distributed Cache** | - | **分布式缓存**。独立服务,通过网络访问(如 Redis)。 |
|
||
| **Consistent Hashing** | - | **一致性哈希**。分布式缓存中用于数据分片和节点扩缩容的算法。 |
|