缓存知识点总结

缓存知识点总结

本文档系统性地梳理缓存的核心概念、工作原理、常见模式、典型问题、一致性策略与最佳实践,适用于后端开发、架构设计与面试复习。


目录

  1. 缓存基础概念
  2. 缓存的作用与优势
  3. 缓存的工作原理
  4. 缓存的分类
  5. 常见的缓存读写策略
  6. 缓存淘汰策略
  7. 缓存三大经典问题
  8. 缓存与数据库一致性
  9. 常见缓存技术对比
  10. 缓存高可用与集群
  11. 缓存最佳实践
  12. 典型应用场景
  13. 常见面试题

一、缓存基础概念

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-ControlExpiresETagLast-Modified 控制
  • CDN 缓存:边缘节点缓存静态资源
  • 反向代理缓存:Nginx 缓存、Varnish 等
  • 网关缓存:API 网关层的响应缓存

五、常见的缓存读写策略

5.1 Cache Aside(旁路缓存)------ 最常用

应用代码同时维护缓存与数据库,缓存由调用方主动管理。

读流程

  1. 先查缓存,命中则返回
  2. 未命中查数据库,并将结果写入缓存

写流程

  1. 先更新数据库,再删除缓存(推荐)

    // 伪代码
    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 请求,可能压垮数据库。

解决方案

  1. 缓存空值/默认值 :即使数据库未查到,也将 null 缓存(设置较短过期时间)

    复制代码
    cache.put(key, NULL, 60s);
  2. 布隆过滤器(Bloom Filter):在缓存前加一层布隆过滤器,请求先过过滤器判断 Key 是否可能存在

  3. 接口限流与参数校验:对非法请求(如 ID < 0)直接拦截

7.2 缓存击穿(Cache Breakdown)

定义热点 Key 在过期的瞬间,大量并发请求同时到达,全部穿透到数据库。

危害:瞬时高并发请求压垮数据库。

解决方案

  1. 互斥锁(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);
        }
    }
  2. 热点 Key 永不过期:不设置过期时间,通过后台异步更新

  3. 逻辑过期:在 Value 中存逻辑过期时间,到期后台异步刷新

7.3 缓存雪崩(Cache Avalanche)

定义大量 Key 同时失效缓存服务整体宕机,导致海量请求穿透到数据库。

危害:数据库瞬时负载激增,可能引发连锁故障。

解决方案

  1. 过期时间打散 :在基础过期时间上加随机值,避免同时失效

    复制代码
    expire = baseExpire + random(0, 300s)
  2. 缓存高可用:Redis 集群、哨兵模式,避免单点故障

  3. 多级缓存:本地缓存 + 分布式缓存兜底

  4. 限流降级:数据库层加限流,缓存不可用时返回降级数据

  5. 缓存预热:系统启动时提前加载热点数据

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:为什么写操作要删除缓存而不是更新缓存?

  1. 避免并发写导致数据不一致(更新顺序不可控)
  2. 节省资源:部分缓存可能从未被读,更新是浪费
  3. 懒加载思想:用到时再加载,数据更"新鲜"

Q2:如何保证缓存与数据库的最终一致性?

:采用 Cache Aside(先更新 DB,再删缓存) + 消息队列重试binlog 订阅(Canal)异步删缓存,保证删除操作最终成功。

Q3:缓存穿透、击穿、雪崩的区别与解决方案?

第七节

Q4:Redis 为什么这么快?

  1. 基于内存,纯内存操作
  2. 单线程模型,避免上下文切换与锁竞争
  3. IO 多路复用(epoll)
  4. 高效数据结构(SDS、跳表、压缩列表等)
  5. 6.0 引入多线程处理网络 IO

Q5:如何排查线上缓存命中率突然下降?

  1. 检查是否有大 Key 被淘汰
  2. 检查过期策略是否变化
  3. 检查是否有异常流量(如大量冷数据请求)
  4. 检查缓存容量是否不足
  5. 检查 Key 设计是否合理(如随机性过高)

Q6:分布式锁如何用缓存实现?

基于 Redis 的 SET key value NX PX 实现,注意:

  • 设置超时时间避免死锁
  • Value 设为唯一标识,释放时用 Lua 脚本保证原子性
  • 生产建议使用 Redisson(支持 watchdog 续期)

参考资料

相关推荐
运行时异常12 分钟前
【WMS 仓储系统集成 AI Agent 实战】第 3 讲:Spring Security 6 + JWT——addFilterBefore 一词之差,全站 401
java·后端
2601_9620715723 分钟前
如何利用SpringSecurity进行认证与授权
java·服务器·数据库
2601_9620697725 分钟前
SpringBoot开发——初步了解SpringBoot
java·spring boot·后端
旧梦952731 分钟前
Java 单例模式:从基础到实战的完整指南
java·开发语言·单例模式
jay神41 分钟前
【计算机毕业设计】基于Spring Boot的宠物领养管理系统
java·spring boot·后端·vue·毕业设计·宠物
cfm_29141 小时前
5种Java并发同步工具全解
java
十五喵源码网1 小时前
基于SpringBoot2+vue2的健身房管理系统
java·毕业设计·springboot·论文笔记
2601_962056601 小时前
可造成敏感信息泄露!Spring Boot之Actuator信息泄露漏洞三种利用方式总结
java·spring boot·后端
garmin Chen2 小时前
redis面试题
java·数据库·redis·缓存