缓存知识点总结
本文档系统性地梳理缓存的核心概念、工作原理、常见模式、典型问题、一致性策略与最佳实践,适用于后端开发、架构设计与面试复习。
目录
- 缓存基础概念
- 缓存的作用与优势
- 缓存的工作原理
- 缓存的分类
- 常见的缓存读写策略
- 缓存淘汰策略
- 缓存三大经典问题
- 缓存与数据库一致性
- 常见缓存技术对比
- 缓存高可用与集群
- 缓存最佳实践
- 典型应用场景
- 常见面试题
一、缓存基础概念
1.1 什么是缓存
缓存(Cache) 是一种用于临时存储高频访问数据的技术或组件,其核心思想是:用空间换时间,通过将数据存放在访问速度更快的存储介质中,减少对慢速存储(如数据库、磁盘、远程服务)的访问次数,从而提升系统整体性能。
1.2 缓存的核心思想
缓存的本质是 数据冗余与就近访问,遵循以下两个基本原则:
- ** locality of reference(局部性原理)**:
- 时间局部性:刚被访问过的数据,很可能在近期再次被访问。
- 空间局部性:被访问数据附近的数据,很可能也将被访问。
- 二八定律(80/20 法则):80% 的请求集中在 20% 的数据上,缓存这 20% 的热点数据即可获得显著收益。
1.3 缓存的位置
缓存可以存在于计算机体系的各个层级,形成 多级缓存体系:
CPU L1/L2/L3 缓存 → 内存 → 本地缓存 → 分布式缓存 → 数据库 → 文件系统
快 ←------------------------------------------------------------------------------------------------------------------------→ 慢
贵 ←------------------------------------------------------------------------------------------------------------------------→ 便宜
小 ←------------------------------------------------------------------------------------------------------------------------→ 大
二、缓存的作用与优势
2.1 主要作用
| 作用 | 说明 |
|---|---|
| 高性能 | 缓存基于内存,读写速度远超磁盘数据库(Redis ~10万 QPS,MySQL ~数千 QPS) |
| 高并发 | 缓存可承受远高于数据库的并发请求,是系统应对高并发的利器 |
| 降低后端压力 | 减少数据库、远程服务的访问频次,保护下游系统 |
| 降低网络开销 | 本地缓存避免了跨网络调用 |
| 节约成本 | 减少数据库负载可降低对昂贵硬件资源的依赖 |
2.2 缓存的代价
缓存并非"银弹",引入缓存也带来以下成本:
- 数据一致性风险:缓存与数据库存在双写一致性问题。
- 复杂度增加:引入缓存增加了系统架构复杂度、运维成本。
- 内存消耗:缓存需要占用额外的内存资源。
- 运维成本:缓存集群的监控、扩容、故障排查等。
- 潜在可用性风险:缓存宕机可能导致请求直接打到数据库,引发雪崩。
原则 :是否引入缓存,需在 性能收益 与 一致性/复杂度成本 之间权衡。数据一致性要求极高的场景(如金融账户余额)应慎重使用缓存。
三、缓存的工作原理
3.1 缓存命中与未命中
- Cache Hit(命中):请求的数据在缓存中存在,直接返回。
- Cache Miss(未命中):请求的数据不在缓存中,需回源到数据库查询,并将结果写入缓存。
3.2 缓存命中率
缓存命中率(Cache Hit Ratio) = 命中次数 / (命中次数 + 未命中次数)
命中率是衡量缓存效果的核心指标,通常期望达到 80% 以上。影响命中率的关键因素:
- 业务场景与数据访问模式
- 缓存容量与淘汰策略
- Key 的设计与过期时间设置
- 缓存预热与更新策略
3.3 缓存读写基本流程
读流程(Read-Through):
1. 请求查询数据
2. 先查询缓存
3. 命中 → 直接返回
4. 未命中 → 查询数据库 → 写入缓存 → 返回
写流程 :见 第五节。
四、缓存的分类
4.1 按部署方式分类
1. 本地缓存(Local Cache)
数据存储在应用进程内或本机内存中。
- 代表实现:Caffeine、Guava Cache、Ehcache、Spring Cache(本地模式)
- 优点 :
- 访问速度极快(纳秒级),无网络开销
- 简单易用,无外部依赖
- 缺点 :
- 容量受限于单机内存
- 多实例间数据不共享,存在数据不一致
- 应用重启缓存丢失
2. 分布式缓存(Distributed Cache)
数据存储在独立的缓存服务集群中,多应用实例共享。
- 代表实现:Redis、Memcached
- 优点 :
- 容量大,可水平扩展
- 多实例共享数据,一致性更好
- 独立部署,与应用解耦
- 缺点 :
- 有网络开销(毫秒级)
- 架构复杂,需要集群与高可用方案
- 存在序列化/反序列化开销
4.2 按层级分类(多级缓存)
实际生产中常采用 多级缓存架构:
请求 → L1 本地缓存(Caffeine) → L2 分布式缓存(Redis) → 数据库
- L1 夂贴近应用,速度最快,用于拦截绝大多数高频请求
- L2 作为共享数据层,保证多实例一致性
- 数据库作为最终数据来源
4.3 按 CDN/HTTP 缓存分类
- 浏览器缓存 :通过
Cache-Control、Expires、ETag、Last-Modified控制 - CDN 缓存:边缘节点缓存静态资源
- 反向代理缓存:Nginx 缓存、Varnish 等
- 网关缓存:API 网关层的响应缓存
五、常见的缓存读写策略
5.1 Cache Aside(旁路缓存)------ 最常用
应用代码同时维护缓存与数据库,缓存由调用方主动管理。
读流程:
- 先查缓存,命中则返回
- 未命中查数据库,并将结果写入缓存
写流程:
-
先更新数据库,再删除缓存(推荐)
// 伪代码
public Data read(Long id) {
Data data = cache.get(id);
if (data == null) {
data = db.query(id);
if (data != null) {
cache.put(id, data);
}
}
return data;
}public void update(Data data) {
db.update(data);
cache.delete(data.getId()); // 删除而非更新
}
要点:
- 写操作建议 删除缓存 而非更新缓存,避免并发写导致的数据不一致
- 这也是业界主流方案(如 Facebook 论文《Scaling Memcache at Facebook》)
5.2 Read/Write Through(读穿/写穿)
应用只与缓存交互,缓存组件负责同步回源到数据库。
- Read Through:未命中时,由缓存组件自动从数据库加载并回填
- Write Through:写入时同步更新缓存与数据库
特点:对应用透明,但实现复杂,通常需要缓存中间件支持(如 Guava LoadingCache)。
5.3 Write Behind / Write Back(异步回写)
写入时只更新缓存,由后台异步批量刷新到数据库。
- 优点:写性能极高,适合写密集场景
- 缺点:数据可能丢失,一致性弱
适用场景:计数器、日志、监控数据等容忍数据丢失的场景。
5.4 三种策略对比
| 策略 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| Cache Aside | 较强 | 高 | 低 | 通用场景,最常用 |
| Read/Write Through | 强 | 中 | 中 | 缓存组件支持的场景 |
| Write Behind | 弱 | 极高 | 高 | 写密集、容忍丢数据 |
六、缓存淘汰策略
当缓存容量达到上限时,需根据淘汰策略删除部分数据。常见策略:
6.1 常见淘汰策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| FIFO(First In First Out) | 先入先出,淘汰最早进入的 | 简单场景,少用 |
| LRU(Least Recently Used) | 淘汰最久未使用 | 通用,最常用,符合时间局部性 |
| LFU(Least Frequently Used) | 淘汰使用频率最低 | 热点数据明显的场景 |
| LRU-K | 综合最近 K 次访问 | 优化 LRU 的"缓存污染"问题 |
| ARC(Adaptive Replacement Cache) | 自适应 LRU 与 LFU | 复杂度高,特定场景 |
| Random(随机淘汰) | 随机删除 | 实现简单,性能稳定 |
| TTL(Time To Live) | 按过期时间淘汰 | 数据有时效性 |
6.2 Redis 的 8 种淘汰策略
| 策略 | 范围 | 算法 |
|---|---|---|
noeviction |
不淘汰 | 内存满时写入直接报错 |
allkeys-lru |
所有 Key | LRU |
allkeys-lfu |
所有 Key | LFU |
allkeys-random |
所有 Key | 随机 |
volatile-lru |
设置过期时间的 Key | LRU |
volatile-lfu |
设置过期时间的 Key | LFU |
volatile-random |
设置过期时间的 Key | 随机 |
volatile-ttl |
设置过期时间的 Key | TTL 最短优先 |
Redis 4.0+ 引入 LFU,适用于热点数据明显的场景。
6.3 LRU 简易实现思路
LRU 通常基于 HashMap + 双向链表 实现,保证 O(1) 的 get/put:
- HashMap:Key → Node,O(1) 查找
- 双向链表:维护访问顺序,最近访问的移到头部,尾部为最久未使用
- 容量满时删除尾部节点
七、缓存三大经典问题
7.1 缓存穿透(Cache Penetration)
定义 :请求查询一个 根本不存在 的数据,缓存和数据库都没有,每次请求都打到数据库。
危害:恶意攻击者用大量不存在的 Key 请求,可能压垮数据库。
解决方案:
-
缓存空值/默认值 :即使数据库未查到,也将
null缓存(设置较短过期时间)cache.put(key, NULL, 60s); -
布隆过滤器(Bloom Filter):在缓存前加一层布隆过滤器,请求先过过滤器判断 Key 是否可能存在
-
接口限流与参数校验:对非法请求(如 ID < 0)直接拦截
7.2 缓存击穿(Cache Breakdown)
定义 :热点 Key 在过期的瞬间,大量并发请求同时到达,全部穿透到数据库。
危害:瞬时高并发请求压垮数据库。
解决方案:
-
互斥锁(Mutex Lock) :未命中时只允许一个线程回源查库,其他线程等待
// 伪代码 if ((data = cache.get(key)) == null) { if (tryLock(key)) { // 获取分布式锁 try { if ((data = cache.get(key)) == null) { data = db.query(key); cache.put(key, data); } } finally { unlock(key); } } else { sleep(50); // 短暂等待后重试 return read(key); } } -
热点 Key 永不过期:不设置过期时间,通过后台异步更新
-
逻辑过期:在 Value 中存逻辑过期时间,到期后台异步刷新
7.3 缓存雪崩(Cache Avalanche)
定义 :大量 Key 同时失效 或 缓存服务整体宕机,导致海量请求穿透到数据库。
危害:数据库瞬时负载激增,可能引发连锁故障。
解决方案:
-
过期时间打散 :在基础过期时间上加随机值,避免同时失效
expire = baseExpire + random(0, 300s) -
缓存高可用:Redis 集群、哨兵模式,避免单点故障
-
多级缓存:本地缓存 + 分布式缓存兜底
-
限流降级:数据库层加限流,缓存不可用时返回降级数据
-
缓存预热:系统启动时提前加载热点数据
7.4 三大问题对比
| 问题 | 触发原因 | 核心特征 | 主流方案 |
|---|---|---|---|
| 穿透 | 查询不存在的数据 | 数据本身不存在 | 空值缓存 + 布隆过滤器 |
| 击穿 | 热点 Key 过期 | 单个 Key 失效 | 互斥锁 + 永不过期 |
| 雪崩 | 大量 Key 同时失效 | 大面积失效 | 过期时间打散 + 高可用 |
八、缓存与数据库一致性
8.1 一致性问题来源
缓存与数据库是两个独立存储,无法在一个事务内原子更新,必然存在短暂的不一致窗口。
8.2 常见方案对比
方案 1:先更新数据库,再更新缓存
问题:并发写场景下,A、B 两个线程交错执行可能导致缓存与数据库不一致:
线程A 更新 DB = 1
线程B 更新 DB = 2
线程B 更新缓存 = 2
线程A 更新缓存 = 1 ← 缓存为 1,DB 为 2,不一致!
方案 2:先更新数据库,再删除缓存(Cache Aside,推荐)
写操作删除缓存,下次读会重新加载,避免并发更新问题。
方案 3:先删除缓存,再更新数据库
问题:删缓存后、更新数据库前,有线程读取并回填旧数据:
线程A 删除缓存
线程B 读缓存未命中 → 读 DB 旧值 → 写入缓存(旧值)
线程A 更新数据库 ← 缓存为旧值,不一致!
改进 :延迟双删 ------ 先删缓存、更新 DB、休眠一段时间、再次删缓存。
1. 删除缓存
2. 更新数据库
3. sleep(N ms) // 等待读请求完成回填
4. 再次删除缓存
8.3 终极方案:消息队列 + 重试
为解决删除缓存失败导致的不一致,可借助消息队列保证最终一致性:
更新DB → 删除缓存 → 失败? → 投递 MQ → 消费者重试删除
或订阅数据库 binlog(如 Canal)异步删除缓存:
DB → binlog → Canal → 解析变更 → 删除对应缓存
8.4 一致性方案选型建议
| 场景 | 推荐方案 |
|---|---|
| 一般业务,容忍短暂不一致 | Cache Aside(先更新 DB,再删除缓存) |
| 强一致要求 | 不缓存 / 短 TTL + 加锁 / binlog 同步 |
| 删缓存易失败 | MQ 重试 / binlog 订阅 |
核心原则:先 DB 后 Cache;删除而非更新;保证删除成功(重试/binlog)。
九、常见缓存技术对比
9.1 Redis vs Memcached
| 特性 | Redis | Memcached |
|---|---|---|
| 数据结构 | String/Hash/List/Set/ZSet/Stream 等 | 仅 KV(String) |
| 持久化 | 支持 RDB/AOF | 不支持 |
| 集群 | 原生 Cluster、哨兵 | 客户端分片 |
| 线程模型 | 单线程(6.0 多线程 IO) | 多线程 |
| 内存效率 | 较高 | 更高(结构简单) |
| 适用场景 | 复杂业务、需要持久化 | 纯 KV、大容量简单缓存 |
9.2 本地缓存对比
| 特性 | Caffeine | Guava Cache | Ehcache |
|---|---|---|---|
| 性能 | 最优(W-TinyLFU) | 良好 | 一般 |
| 淘汰算法 | W-TinyLFU(接近最优命中率) | LRU | LRU/LFU |
| 堆外存储 | 支持 | 不支持 | 支持 |
| 推荐度 | ⭐⭐⭐⭐⭐(Spring Boot 默认) | 已停更 | ⭐⭐⭐ |
选型建议 :本地缓存首选 Caffeine (Spring Boot 2.x+ 默认),分布式缓存首选 Redis。
9.3 其他缓存
- Hazelcast:分布式内存数据网格,适合 IMDG 场景
- Tair:阿里开源分布式 KV,支持多副本与持久化
- Pika:大容量 Redis 兼容方案(基于 RocksDB)
- Codis:豌豆荚 Redis 集群方案(已逐步被原生 Cluster 替代)
十、缓存高可用与集群
10.1 Redis 高可用方案
1. 主从复制(Master-Slave)
- 一主多从,主写从读,读写分离
- 主节点故障需手动切换
2. 哨兵模式(Sentinel)
- 哨兵集群监控主从
- 主节点宕机自动选举新主
- 适用于小规模集群
3. Cluster 集群
- 数据分片(16384 个槽位)
- 节点间 Gossip 协议通信
- 每个分片自带主从
- 适用于大规模数据与高并发
10.2 数据分片策略
- 哈希取模 :
hash(key) % N,扩缩容迁移量大 - 一致性哈希:节点增减只影响相邻数据,常配虚拟节点
- Redis Cluster 槽位 :
CRC16(key) % 16384,按槽位分配节点
10.3 缓存故障的容错
- 熔断降级:缓存不可用时返回降级数据,避免压垮 DB
- 限流:Hystrix/Sentinel 限制并发,保护后端
- 本地兜底:分布式缓存不可用时退化为本地缓存
十一、缓存最佳实践
11.1 Key 设计
- 命名规范 :
业务:对象:属性,如user:profile:1001 - 长度适中:避免过长 Key 占用内存
- 可读性:便于排查与监控
- 避免大 Key:单个 Value 控制在 10KB 以内,否则拆分
11.2 Value 设计
- 避免大 Value:Hash/List/Set 元素过多时拆分
- 合理序列化:JSON / Protobuf / Hessian,权衡可读性与体积
- 设置合理过期时间:避免内存膨胀,加随机值防雪崩
11.3 缓存粒度
- 缓存对象:常用,简单
- 缓存集合:避免大 List,考虑分页或 Hash 分片
- 缓存计算结果:聚合查询结果缓存,避免重复计算
11.4 监控指标
| 指标 | 说明 | 告警阈值建议 |
|---|---|---|
| 命中率 | hit / (hit + miss) | < 80% 需关注 |
| 内存使用率 | used / max | > 80% 关注 |
| QPS | 每秒请求数 | 接近上限告警 |
| 慢查询 | 执行时间长的命令 | > 10ms 告警 |
| 大 Key | 单 Key 体积 | > 10KB 关注 |
| 热 Key | 访问频次极高 | 单 Key > 1万 QPS |
11.5 常见禁忌
- ❌ 将缓存当作主存储(数据可能丢失)
- ❌ 缓存价值过大的对象(大 Key 问题)
- ❌ 缓存不过期(内存膨胀)
- ❌ 写操作更新缓存(应删除)
- ❌ 全部数据加同一过期时间(雪崩)
- ❌ 缓存层不做熔断降级(缓存宕机击穿 DB)
十二、典型应用场景
12.1 商品详情页
- 商品基本信息、SKU、价格 → Redis 缓存
- 静态资源 → CDN
- 多级缓存:本地 + Redis + DB
12.2 秒杀/抢购
- 库存预扣减 → Redis 原子操作
- 限流 → 令牌桶 / Sentinel
- 异步下单 → MQ 削峰
12.3 排行榜
- Redis Sorted Set 实时排序
- 定时刷新到 DB
12.4 会话存储(Session)
- 分布式 Session → Redis 集中存储
- 解决多实例 Session 共享问题
12.5 计数与限流
- 点赞数、评论数 → Redis INCR
- 接口限流 → 滑动窗口 / 令牌桶(基于 Redis)
12.6 热点数据
- 微博热搜、新闻热点
- 本地缓存 + 短 TTL + 热点探测
十三、常见面试题
Q1:为什么写操作要删除缓存而不是更新缓存?
答:
- 避免并发写导致数据不一致(更新顺序不可控)
- 节省资源:部分缓存可能从未被读,更新是浪费
- 懒加载思想:用到时再加载,数据更"新鲜"
Q2:如何保证缓存与数据库的最终一致性?
答 :采用 Cache Aside(先更新 DB,再删缓存) + 消息队列重试 或 binlog 订阅(Canal)异步删缓存,保证删除操作最终成功。
Q3:缓存穿透、击穿、雪崩的区别与解决方案?
见 第七节。
Q4:Redis 为什么这么快?
- 基于内存,纯内存操作
- 单线程模型,避免上下文切换与锁竞争
- IO 多路复用(epoll)
- 高效数据结构(SDS、跳表、压缩列表等)
- 6.0 引入多线程处理网络 IO
Q5:如何排查线上缓存命中率突然下降?
- 检查是否有大 Key 被淘汰
- 检查过期策略是否变化
- 检查是否有异常流量(如大量冷数据请求)
- 检查缓存容量是否不足
- 检查 Key 设计是否合理(如随机性过高)
Q6:分布式锁如何用缓存实现?
基于 Redis 的 SET key value NX PX 实现,注意:
- 设置超时时间避免死锁
- Value 设为唯一标识,释放时用 Lua 脚本保证原子性
- 生产建议使用 Redisson(支持 watchdog 续期)
参考资料
- 《Redis 设计与实现》------ 黄健宏
- 《大型网站技术架构》------ 李智慧
- Facebook Engineering Blog: Scaling Memcache at Facebook
- Redis 官方文档:https://redis.io/docs
- Caffeine GitHub:https://github.com/ben-manes/caffeine