沉梦手记 沉梦手记
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 基础篇
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • 网络通信
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 音视频处理
  • Linux 与容器化运维
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 随笔
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Axios入门
  • Vue Router入门
  • Pinia入门
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
Github (opens new window)

沉梦听雨

时间是最好的浸渍剂,而沉淀是最好的提纯器🚀
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 基础篇
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • 网络通信
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 音视频处理
  • Linux 与容器化运维
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 随笔
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Axios入门
  • Vue Router入门
  • Pinia入门
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
Github (opens new window)
  • 设计须知

    • 命名规范
    • 聊聊什么是耦合度
    • 幂等性问题分析
    • LocalDateTime和DateTime
    • 聊聊软件架构设计
    • 说说war和jar的区别
    • 聊聊多租户是什么
    • 加密算法分析
    • 那些关于管理系统的知识
    • 唯一索引和逻辑删除冲突解决方法
    • 日志记录相关
    • 过滤器、拦截器、AOP切面与其他拦截机制
    • 缓存设计分析
      • 概述
        • 为什么需要缓存?
        • 缓存的代价
      • 全链路缓存架构
        • 各层缓存总览
      • ① 浏览器缓存(HTTP 缓存)
        • 基本概念
        • 强缓存 vs 协商缓存
        • 强缓存
        • 协商缓存
        • 缓存决策流程
        • 前端工程化中的缓存策略
      • ② CDN 缓存
        • 基本概念
        • CDN 的价值
        • CDN 缓存控制
        • CDN 缓存策略建议
        • CDN 回源策略
      • ③ Nginx 缓存
        • 基本概念
        • Nginx 缓存配置
        • 关键配置说明
        • $upstream_cache_status 状态值
        • Nginx 缓存清除
        • Nginx 缓存适用场景
      • ④ 应用层缓存 — 本地缓存
        • 基本概念
        • 常用实现方案
        • 方案一:自己写 Map(不推荐)
        • 方案二:Caffeine(推荐)
        • 方案三:Spring Cache + Caffeine
        • 本地缓存适用场景
      • ④ 应用层缓存 — 分布式缓存(Redis)
        • 基本概念
        • Redis 常用数据结构选型
        • Spring Boot 集成 Redis
        • 配置
        • 使用 RedisTemplate
        • 使用 Spring Cache + Redis
        • Redis 缓存适用场景
      • ⑤ 数据库缓存
        • MySQL Buffer Pool
        • 各层缓存对比
      • 多级缓存架构
        • 设计理念
        • 多级缓存代码实现
        • 各层缓存的职责划分
      • 缓存更新策略
        • 策略对比
        • Cache Aside 详解(推荐)
        • 延迟双删
        • 多级缓存的更新问题
      • 缓存三大经典问题
        • 缓存穿透
        • 缓存击穿
        • 缓存雪崩
        • 三大问题对比
      • 缓存淘汰策略
      • 实际场景分析
        • 场景一:电商商品详情页(全链路缓存)
        • 场景二:用户信息缓存
        • 场景三:排行榜
        • 场景四:验证码 / Token
        • 场景五:字典数据 / 配置缓存
        • 场景六:静态资源发布(前端 + CDN)
      • 缓存使用注意事项
        • 1. 大 Key 问题
        • 2. 热 Key 问题
        • 3. 缓存预热
        • 4. 缓存监控
        • 5. 序列化选择
      • 总结
        • 全链路缓存总览
        • 设计要点
  • UML画图

  • 权限校验

  • 设计模式

  • 系统设计
  • 设计须知
沉梦听雨
2026-08-14
目录

缓存设计分析

# 缓存设计分析

# 概述

缓存是提升系统性能、降低后端压力的核心手段。其本质是用空间换时间——将频繁访问的数据存储在更快的介质中,减少对慢速数据源的访问。

一句话总结:缓存不是银弹,用得好是加速器,用不好是定时炸弹。

# 为什么需要缓存?

痛点 缓存如何解决
数据库 QPS 过高 热点数据缓存后,读请求直接命中缓存,不查数据库
接口响应慢 缓存读取耗时 < 1ms,数据库读取耗时 10~100ms
高并发下数据库扛不住 缓存可承受 10 万+ QPS,远超数据库
带宽瓶颈 静态资源缓存在 CDN / 浏览器,减少回源带宽
重复计算开销大 计算结果缓存,避免重复计算

# 缓存的代价

代价 说明
数据不一致 数据源更新后缓存未同步,读到旧数据
内存 / 存储成本 缓存数据占用额外内存或磁盘
运维复杂度 缓存集群维护、监控、故障排查
缓存问题 缓存穿透、缓存击穿、缓存雪崩
架构复杂度 多级缓存引入更多组件,排查链路变长

# 全链路缓存架构

从系统架构设计的角度,缓存不是某一层的技术,而是贯穿整个请求链路的设计理念。一个用户请求从浏览器发出到数据库返回,中间会经过多层缓存:

用户浏览器
    │
    ▼
┌─────────────────────────────────────────────────────────┐
│  ① 浏览器缓存(HTTP 缓存)                               │
│  ┌───────────────────────────────────────────────────┐  │
│  │  ② CDN 缓存(边缘节点)                            │  │
│  │  ┌─────────────────────────────────────────────┐  │  │
│  │  │  ③ Nginx 缓存(反向代理缓存)                │  │  │
│  │  │  ┌───────────────────────────────────────┐  │  │  │
│  │  │  │  ④ 应用层缓存                          │  │  │  │
│  │  │  │  ┌─────────────────────────────────┐  │  │  │  │
│  │  │  │  │  本地缓存(Caffeine)            │  │  │  │  │
│  │  │  │  │  ┌───────────────────────────┐  │  │  │  │  │
│  │  │  │  │  │  分布式缓存(Redis)       │  │  │  │  │  │
│  │  │  │  │  │  ┌─────────────────────┐  │  │  │  │  │  │
│  │  │  │  │  │  │  ⑤ 数据库缓存        │  │  │  │  │  │  │
│  │  │  │  │  │  │  (Buffer Pool)     │  │  │  │  │  │  │
│  │  │  │  │  │  └─────────────────────┘  │  │  │  │  │  │
│  │  │  │  │  └───────────────────────────┘  │  │  │  │  │
│  │  │  │  └─────────────────────────────────┘  │  │  │  │
│  │  │  └───────────────────────────────────────┘  │  │  │
│  │  └─────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
    │
    ▼
HTTP 响应(逐层返回,每层可命中缓存直接返回)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 各层缓存总览

层级 位置 缓存介质 典型耗时 缓存内容 失效方式
① 浏览器缓存 用户设备 磁盘 / 内存 ~0ms 静态资源(JS/CSS/图片) Cache-Control / ETag
② CDN 缓存 边缘节点 CDN 节点内存 / 磁盘 5~20ms 静态资源、页面片段 TTL / 主动刷新
③ Nginx 缓存 反向代理服务器 磁盘 / 共享内存 1~5ms 接口响应、静态文件 TTL / purge
④ 本地缓存 应用 JVM 堆内存 纳秒级 热点数据、字典数据 TTL / LRU 淘汰
④ 分布式缓存 Redis 集群 内存 1~2ms 业务数据、Session TTL / 主动删除
⑤ 数据库缓存 数据库引擎 内存(Buffer Pool) 微秒级 数据页、索引页 LRU 淘汰

设计原则:请求从外到内逐层穿透,每一层能拦截的请求就不要放到下一层。最外层(浏览器)拦截 90% 的静态资源请求,CDN 拦截 80% 的回源请求,Nginx 拦截热点接口,应用层缓存拦截数据库查询。

# ① 浏览器缓存(HTTP 缓存)

# 基本概念

浏览器缓存是最外层的缓存,存储在用户设备的磁盘或内存中。对于静态资源(JS、CSS、图片、字体),合理配置浏览器缓存可以完全避免 HTTP 请求,是性能优化的第一道防线。

# 强缓存 vs 协商缓存

浏览器缓存分为两种机制:

类型 特点 命中时行为 相关 Header
强缓存 不发请求,直接用本地缓存 200(from cache) Cache-Control / Expires
协商缓存 发请求询问服务器资源是否变更 304(Not Modified) ETag / Last-Modified

# 强缓存

# 响应头
Cache-Control: max-age=31536000, public, immutable
Expires: Wed, 14 Aug 2027 10:00:00 GMT
1
2
3
Cache-Control 值 说明
max-age=31536000 缓存有效期 1 年(秒)
public 允许 CDN 等中间代理缓存
private 仅浏览器可缓存(如用户个人数据)
no-cache 不直接用缓存,每次需协商验证
no-store 完全不缓存(敏感数据)
immutable 资源永不变,URL 变化时才重新请求

💡 Cache-Control 优先级高于 Expires,Expires 是 HTTP/1.0 的产物,现在作为兜底。

# 协商缓存

# 第一次响应
ETag: "abc123"
Last-Modified: Wed, 14 Aug 2026 10:00:00 GMT

# 第二次请求(带上缓存标识)
If-None-Match: "abc123"
If-Modified-Since: Wed, 14 Aug 2026 10:00:00 GMT

# 服务器判断:
#   未变更 → 返回 304 Not Modified(不返回 body,省带宽)
#   已变更 → 返回 200 + 新资源 + 新 ETag
1
2
3
4
5
6
7
8
9
10
11
标识 说明 优先级
ETag / If-None-Match 基于内容哈希,精确判断资源是否变化 高
Last-Modified / If-Modified-Since 基于修改时间,精度为秒 低

# 缓存决策流程

请求资源
    │
    ▼
浏览器有缓存?
    │ 否 → 发请求,下载资源
    │ 是
    ▼
强缓存有效(max-age 未过期)?
    │ 是 → 200 from cache(不发请求)
    │ 否
    ▼
发请求,带 If-None-Match / If-Modified-Since
    │
    ▼
服务器判断资源是否变更?
    │ 未变更 → 304 Not Modified(用本地缓存)
    │ 已变更 → 200 + 新资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 前端工程化中的缓存策略

资源类型 缓存策略 说明
HTML 文件 no-cache 每次协商,确保用户拿到最新页面
带 hash 的 JS/CSS max-age=31536000, immutable 文件名含 hash,内容变则 URL 变
图片 / 字体 max-age=2592000 缓存 30 天
API 响应 no-store 接口数据不缓存(由应用层缓存处理)

💡 Webpack / Vite 的 contenthash 是浏览器缓存的最佳实践:文件内容不变则 hash 不变,浏览器直接用缓存;内容变了则 hash 变,浏览器重新下载。

# ② CDN 缓存

# 基本概念

CDN(Content Delivery Network,内容分发网络)将静态资源缓存到离用户最近的边缘节点,用户请求时由最近的节点直接返回,无需回源到源站服务器。

用户(北京)──→ CDN 边缘节点(北京)──→ 命中缓存,直接返回
                                    │ 未命中
                                    ▼
                              源站服务器(深圳)
                                    │
                                    ▼
                              回源获取 + 缓存到边缘节点
1
2
3
4
5
6
7

# CDN 的价值

价值 说明
加速访问 就近返回,减少网络延迟
减轻源站压力 边缘节点拦截 80%+ 请求
抗DDoS 流量分散到边缘节点
节省带宽 源站只需处理回源请求

# CDN 缓存控制

CDN 缓存遵循 HTTP 缓存头,也可通过 CDN 控制台自定义:

控制方式 说明
Cache-Control: max-age 源站响应头控制 CDN 缓存时间
CDN 控制台配置 覆盖源站头,按文件类型设置缓存时间
主动刷新 发布新版本后,调用 CDN API 清除缓存
URL 版本号 https://cdn.example.com/app.v1.2.0.js,版本号变更即新 URL

# CDN 缓存策略建议

资源类型 CDN 缓存时间 说明
带 hash 的静态资源 30 天 ~ 1 年 内容变则 URL 变,可长期缓存
不带 hash 的静态资源 10 分钟 ~ 1 小时 需定期回源检查
HTML 页面 不缓存或短缓存 确保用户拿到最新页面
API 接口 不缓存 由应用层处理

# CDN 回源策略

当 CDN 节点缓存未命中时,需要回源获取:

策略 说明
普通回源 CDN 节点直接向源站请求
CDN 间回源 先向其他 CDN 节点请求,再回源站
回源缓存 回源获取后缓存到当前节点,下次直接命中

⚠️ CDN 回源会产生额外延迟,应通过合理的缓存时间尽量减少回源率。一般 CDN 命中率应 > 90%。

# ③ Nginx 缓存

# 基本概念

Nginx 作为反向代理服务器,可以缓存后端应用的响应结果。当相同请求再次到达时,Nginx 直接返回缓存内容,无需转发到后端应用。

客户端 → Nginx → 命中缓存 → 直接返回
                │ 未命中
                ▼
            后端应用 → 返回响应 → Nginx 缓存 → 返回客户端
1
2
3
4

# Nginx 缓存配置

# http 块中定义缓存路径
http {
    # 定义缓存区:路径 / 缓存目录层级 / 缓存键区大小 / 最大大小 / 不活跃时间
    proxy_cache_path /data/nginx/cache
                     levels=1:2
                     keys_zone=api_cache:10m
                     max_size=1g
                     inactive=30m
                     use_temp_path=off;

    server {
        listen 80;

        # 对 API 接口启用缓存
        location /api/ {
            proxy_cache api_cache;
            proxy_cache_key "$scheme$request_method$host$request_uri";
            proxy_cache_valid 200 10m;    # 200 响应缓存 10 分钟
            proxy_cache_valid 404 1m;     # 404 缓存 1 分钟
            proxy_cache_valid 500 0;      # 500 不缓存

            # 添加响应头标识是否命中缓存
            add_header X-Cache-Status $upstream_cache_status;

            proxy_pass http://backend;
        }

        # 静态资源缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
            proxy_cache api_cache;
            proxy_cache_valid 200 30d;
            proxy_cache_key $uri;
            proxy_pass http://backend;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 关键配置说明

配置项 说明
proxy_cache_path 定义缓存存储路径和参数
levels=1:2 缓存目录层级,避免单目录文件过多
keys_zone 缓存键共享内存区大小(10m ≈ 8 万个 key)
max_size 缓存磁盘最大占用
inactive=30m 30 分钟未被访问的缓存自动清除
proxy_cache_valid 按状态码设置缓存时间
proxy_cache_key 缓存键的组成(默认含 method + host + uri)

# $upstream_cache_status 状态值

状态 说明
MISS 未命中缓存,请求转发到后端
BYPASS 绕过缓存(配置了 proxy_cache_bypass)
EXPIRED 缓存已过期,请求转发到后端
STALE 缓存已过期,但使用了旧缓存(配置了 proxy_cache_use_stale)
UPDATING 缓存已过期,正在更新,返回旧缓存
REVALIDATED 协商缓存验证通过
HIT 命中缓存

# Nginx 缓存清除

# 允许通过特定请求清除缓存
location /purge/ {
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
    proxy_cache_purge api_cache $scheme$request_method$host$request_uri;
}
1
2
3
4
5
6
7
# 清除指定 URL 的缓存
curl -X GET http://nginx-host/purge/api/user/1
1
2

# Nginx 缓存适用场景

场景 缓存时间 说明
首页 / 列表页接口 1~5 分钟 短缓存,兼顾性能和实时性
商品详情接口 10~30 分钟 读多写少,可容忍短暂不一致
静态资源 30 天 带 hash 的静态文件
用户个人数据 不缓存 个性化数据,不可缓存
搜索接口 不缓存 查询条件多变,缓存命中率低

⚠️ Nginx 缓存是磁盘缓存,读取速度比 Redis 慢,但能承受极高并发,适合缓存大体积响应(如页面、图片)。

# ④ 应用层缓存 — 本地缓存

# 基本概念

本地缓存是运行在应用进程内的缓存,数据存储在 JVM 堆内存中。

  • 优点:访问速度极快(纳秒级),无网络开销
  • 缺点:多实例间数据不共享、受 JVM 内存限制、应用重启后丢失

# 常用实现方案

# 方案一:自己写 Map(不推荐)

private static final Map<String, String> CACHE = new ConcurrentHashMap<>();

public String get(String key) {
    return CACHE.computeIfAbsent(key, k -> loadDataFromDB(k));
}
1
2
3
4
5

问题:无过期机制、无淘汰策略、无容量限制、无监控,生产环境不推荐。

# 方案二:Caffeine(推荐)

Caffeine 是高性能 Java 本地缓存库,Spring Boot 默认集成。

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                .initialCapacity(100)           // 初始容量
                .maximumSize(1000)              // 最大容量
                .expireAfterWrite(10, TimeUnit.MINUTES)  // 写入后 10 分钟过期
                .expireAfterAccess(5, TimeUnit.MINUTES)  // 访问后 5 分钟过期
                .weakKeys()                     // 使用弱引用存储 key
                .recordStats()                  // 开启统计
                .build();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

淘汰策略对比:

策略 说明 适用场景
expireAfterWrite 写入后固定时间过期 数据更新频率低
expireAfterAccess 最后一次访问后过期 热点数据持续访问
maximumSize 容量上限,LRU 淘汰 控制内存占用
weakKeys / weakValues GC 时自动回收 防止内存泄漏

# 方案三:Spring Cache + Caffeine

@EnableCaching
@SpringBootApplication
public class Application { }

@Service
public class UserService {

    @Cacheable(value = "user", key = "#id")
    public User getUserById(Long id) {
        return userMapper.selectById(id); // 只在缓存未命中时执行
    }

    @CachePut(value = "user", key = "#user.id")
    public User updateUser(User user) {
        userMapper.updateById(user);
        return user; // 更新缓存
    }

    @CacheEvict(value = "user", key = "#id")
    public void deleteUser(Long id) {
        userMapper.deleteById(id); // 删除缓存
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Spring Cache 核心注解:

注解 说明
@Cacheable 查询缓存,未命中则执行方法并缓存结果
@CachePut 执行方法并更新缓存
@CacheEvict 删除缓存
@Caching 组合多个缓存操作
@CacheConfig 类级别公共缓存配置

# 本地缓存适用场景

场景 说明
配置字典数据 数据量小、更新频率低、访问频繁
静态数据 省市区列表、枚举值
热点数据 短时间内高频访问的数据
计算结果缓存 复杂计算结果、报表数据

# ④ 应用层缓存 — 分布式缓存(Redis)

# 基本概念

Redis 是基于内存的分布式 Key-Value 存储,支持多种数据结构,是分布式缓存的事实标准。

  • 优点:多实例共享、支持持久化、丰富的数据结构、高可用
  • 缺点:网络开销(1~2ms)、需维护缓存集群

# Redis 常用数据结构选型

数据结构 适用场景 示例
String 简单 KV 缓存 缓存用户信息 JSON
Hash 对象属性存储 user:1 → {name, age, email}
List 有序列表 消息队列、最新动态
Set 去重集合 标签、共同好友
ZSet 排行榜 热搜榜、积分排名
Bitmap 位操作 签到、布隆过滤器
HyperLogLog 基数统计 UV 统计

# Spring Boot 集成 Redis

# 配置

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: xxx
      database: 0
      lettuce:
        pool:
          max-active: 8
          max-idle: 4
          min-idle: 0
          max-wait: -1ms
1
2
3
4
5
6
7
8
9
10
11
12
13

# 使用 RedisTemplate

@Service
public class CacheService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void set(String key, Object value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    public <T> T get(String key, Class<T> clazz) {
        Object value = redisTemplate.opsForValue().get(key);
        return value != null ? clazz.cast(value) : null;
    }

    public Boolean delete(String key) {
        return redisTemplate.delete(key);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 使用 Spring Cache + Redis

@Service
public class ProductService {

    @Cacheable(value = "product", key = "#id", unless = "#result == null")
    public Product getProductById(Long id) {
        return productMapper.selectById(id);
    }

    @CacheEvict(value = "product", key = "#id")
    public void deleteProduct(Long id) {
        productMapper.deleteById(id);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class RedisCacheConfig {

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Redis 缓存适用场景

场景 说明
用户会话 分布式 Session、Token 存储
热点数据缓存 商品详情、文章内容
排行榜 ZSet 实现实时排名
计数器 点赞数、浏览量
分布式锁 SETNX 实现互斥锁
限流 滑动窗口限流

# ⑤ 数据库缓存

# MySQL Buffer Pool

MySQL InnoDB 引擎自带缓冲池(Buffer Pool),将频繁访问的数据页和索引页缓存在内存中,减少磁盘 I/O。

SQL 查询
    │
    ▼
Buffer Pool 命中?
    │ 是 → 直接返回(内存级速度)
    │ 否 → 从磁盘读取数据页 → 存入 Buffer Pool → 返回
1
2
3
4
5
6
参数 说明 建议值
innodb_buffer_pool_size Buffer Pool 大小 物理内存的 60%~80%
innodb_buffer_pool_instances Buffer Pool 实例数 每 1GB 一个实例

💡 数据库缓存是最后一道缓存,由数据库引擎自动管理,开发者通常无需干预,但需合理配置 Buffer Pool 大小。

# 各层缓存对比

对比项 本地缓存(Caffeine) 分布式缓存(Redis) 数据库缓存(Buffer Pool)
访问速度 纳秒级 毫秒级(1~2ms) 微秒级
容量 受 JVM 堆内存限制 受服务器内存限制 受物理内存限制
多实例共享 ❌ 不共享 ✅ 共享 ❌ 不共享
数据一致性 各实例独立 统一存储 单实例独立
持久化 ❌ 重启丢失 ✅ 支持 RDB / AOF ❌ 重启丢失
运维成本 低 高 中
适用场景 单机热点、静态数据 分布式共享数据 数据页、索引页

# 多级缓存架构

# 设计理念

生产环境通常采用多级缓存架构,每一层缓存拦截不同特征的请求:

请求 → 浏览器缓存(静态资源,0ms)
         │ 未命中
         ▼
      CDN 缓存(静态资源回源,5~20ms)
         │ 未命中
         ▼
      Nginx 缓存(接口响应,1~5ms)
         │ 未命中
         ▼
      本地缓存 Caffeine(热点数据,纳秒级)
         │ 未命中
         ▼
      分布式缓存 Redis(业务数据,1~2ms)
         │ 未命中
         ▼
      数据库 Buffer Pool(数据页,微秒级)
         │ 未命中
         ▼
      磁盘读取(10~100ms)
         │
         ▼
      逐层回写缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 多级缓存代码实现

@Service
public class MultiLevelCacheService {

    @Autowired
    private Cache<String, Object> localCache;  // Caffeine

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public Object get(String key) {
        // 1. 查本地缓存
        Object value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }

        // 2. 查 Redis
        value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            localCache.put(key, value);  // 回写本地缓存
            return value;
        }

        // 3. 查数据库
        value = loadFromDB(key);
        if (value != null) {
            redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
            localCache.put(key, value);
        }
        return value;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 各层缓存的职责划分

缓存层 职责 缓存内容 典型 TTL
浏览器 拦截静态资源请求 JS/CSS/图片/字体 1 年(带 hash)
CDN 就近返回静态资源 静态资源、页面 30 天
Nginx 拦截热点接口请求 接口响应体 1~30 分钟
本地缓存 拦截热点数据查询 字典、配置、热点数据 5~10 分钟
Redis 拦截数据库查询 业务数据 30 分钟 ~ 1 小时
数据库 减少磁盘 I/O 数据页、索引页 自动管理

设计原则:越靠外的缓存,容量越大、速度越快、TTL 越长;越靠内的缓存,数据越精确、TTL 越短。

# 缓存更新策略

缓存和数据源是两个数据源,更新时需要保证一致性。常见策略:

# 策略对比

策略 说明 优点 缺点 适用场景
Cache Aside(旁路缓存) 先更新 DB,再删除 Cache 简单、一致性好 短时间不一致 最常用,推荐
Read/Write Through 应用只操作缓存,缓存同步操作 DB 应用逻辑简单 缓存组件复杂 少见
Write Behind 先更新缓存,异步更新 DB 写性能极高 数据可能丢失 日志、统计

# Cache Aside 详解(推荐)

读:查缓存 → 命中则返回 → 未命中则查 DB → 回写缓存
写:更新 DB → 删除缓存
1
2

为什么是删除缓存而不是更新缓存?

操作 问题
更新缓存 并发写时,A 先更新 DB、B 后更新 DB,但 B 先更新缓存、A 后更新缓存 → 缓存是旧值
删除缓存 下次读时懒加载,天然避免并发更新顺序问题

为什么是先更新 DB 再删除缓存,而不是反过来?

顺序 并发问题
先删缓存再更新 DB 线程 A 删缓存 → 线程 B 读缓存未命中,读 DB 旧值并回写缓存 → 线程 A 更新 DB → 缓存是旧值,不一致
先更新 DB 再删缓存 线程 A 更新 DB → 线程 B 读缓存命中旧值(短暂不一致)→ 线程 A 删缓存 → 下次读会加载新值 → 最终一致

⚠️ 先更新 DB 再删缓存也有极端不一致场景(读线程读到 DB 旧值、写线程更新 DB 并删缓存、读线程回写旧值),但概率极低。如需强一致,可用延迟双删。

# 延迟双删

public void updateData(Long id, Data data) {
    // 1. 先删缓存
    redisTemplate.delete("data:" + id);
    // 2. 更新数据库
    dataMapper.updateById(data);
    // 3. 延迟一段时间后再删一次缓存
    scheduledExecutor.schedule(() -> {
        redisTemplate.delete("data:" + id);
    }, 500, TimeUnit.MILLISECONDS);
}
1
2
3
4
5
6
7
8
9
10

延迟时间需大于一次读操作的耗时(读 DB + 回写缓存),通常 500ms~1s。

# 多级缓存的更新问题

多级缓存架构下,更新更复杂——需要同时清理 Nginx 缓存、本地缓存、Redis 缓存:

数据更新
    │
    ▼
更新数据库
    │
    ├── 删除 Redis 缓存
    ├── 删除 / 失效本地缓存(Caffeine invalidate)
    └── 清除 Nginx 缓存(proxy_cache_purge 或 TTL 过期)
1
2
3
4
5
6
7
8

⚠️ 浏览器缓存和 CDN 缓存通常通过版本号 / hash 管理,不通过主动删除。发布新版本时 URL 变化,旧缓存自然失效。

# 缓存三大经典问题

# 缓存穿透

现象:查询一个不存在的数据,缓存和数据库都没有,每次请求都打到数据库。

场景:恶意攻击,如查询 id = -1 的用户。

解决方案:

方案 说明 优缺点
缓存空值 查不到也缓存 null,设短过期时间 简单,但浪费内存、可能短期不一致
布隆过滤器 请求前先过布隆过滤器,不存在则直接拒绝 内存高效,但有误判率
接口校验 对非法参数直接拦截(如 id < 0) 简单,但无法覆盖所有场景
// 方案一:缓存空值
public User getUser(Long id) {
    String key = "user:" + id;
    Object value = redisTemplate.opsForValue().get(key);
    if (value != null) {
        return "NULL".equals(value) ? null : (User) value;
    }
    User user = userMapper.selectById(id);
    if (user == null) {
        redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
    } else {
        redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
    }
    return user;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 缓存击穿

现象:一个热点 Key 突然过期,大量并发请求同时打到数据库。

场景:秒杀商品缓存过期瞬间。

解决方案:

方案 说明 优缺点
互斥锁 缓存未命中时加锁,只有一个线程查 DB 并回写缓存 简单可靠,但会短暂阻塞
逻辑过期 不设 TTL,在 value 中存过期时间,后台异步刷新 不阻塞,但有短暂不一致
热点 Key 永不过期 热点数据不设过期时间,主动更新 一致性好,但需维护更新逻辑
// 方案一:互斥锁
public User getUserWithLock(Long id) {
    String key = "user:" + id;
    Object value = redisTemplate.opsForValue().get(key);
    if (value != null) {
        return (User) value;
    }

    String lockKey = "lock:user:" + id;
    try {
        Boolean locked = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        if (Boolean.TRUE.equals(locked)) {
            value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                return (User) value;
            }
            User user = userMapper.selectById(id);
            redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
            return user;
        } else {
            Thread.sleep(50);
            return getUserWithLock(id);
        }
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        redisTemplate.delete(lockKey);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 缓存雪崩

现象:大量缓存同时过期,或 Redis 宕机,所有请求打到数据库。

场景:缓存批量设置了相同过期时间,同时失效。

解决方案:

方案 说明
过期时间加随机值 TTL = base + random(0~300s),避免同时过期
多级缓存 本地缓存兜底
熔断降级 数据库压力过大时,返回降级数据
Redis 高可用 哨兵 / 集群模式,避免单点故障
// 过期时间加随机值
int ttl = 1800 + ThreadLocalRandom.current().nextInt(300);
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
1
2
3

# 三大问题对比

问题 本质 触发条件 核心方案
穿透 查不存在的数据 恶意攻击 / bug 缓存空值 / 布隆过滤器
击穿 热点 Key 过期 热点数据过期 互斥锁 / 逻辑过期
雪崩 大量 Key 同时过期 过期时间相同 随机过期 / 多级缓存

# 缓存淘汰策略

当缓存空间不足时,需要淘汰部分数据。Redis 提供以下策略:

策略 说明 适用场景
noeviction 不淘汰,内存满时写入报错 数据不能丢失
allkeys-lru 淘汰最久未使用的 Key 最常用
allkeys-lfu 淘汰使用频率最低的 Key 热点数据明显
allkeys-random 随机淘汰 无差别数据
volatile-lru 仅在设了过期的 Key 中 LRU 淘汰 混合使用场景
volatile-ttl 优先淘汰即将过期的 Key 优先清理短期数据
volatile-random 在设了过期的 Key 中随机淘汰 —

💡 生产环境推荐 allkeys-lru,兼顾性能和命中率。

# 实际场景分析

# 场景一:电商商品详情页(全链路缓存)

特点:读多写少、热点数据、可容忍短暂不一致。

用户访问商品详情页
    │
    ▼
浏览器缓存 HTML(no-cache,每次协商)
    │
    ▼
CDN 缓存静态资源(JS/CSS/图片,30天)
    │
    ▼
Nginx 缓存商品详情接口(10分钟)
    │ 未命中
    ▼
本地缓存 Caffeine(5分钟)
    │ 未命中
    ▼
Redis 缓存商品数据(30分钟)
    │ 未命中
    ▼
数据库
    │
    ▼
逐层回写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class ProductService {

    @Cacheable(value = "product:detail", key = "#id", unless = "#result == null")
    public Product getProductDetail(Long id) {
        return productMapper.selectById(id);
    }

    @CacheEvict(value = "product:detail", key = "#product.id")
    public void updateProduct(Product product) {
        productMapper.updateById(product);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

策略:全链路缓存 + Cache Aside + 空值缓存(防穿透)+ 过期时间加随机值(防雪崩)。

# 场景二:用户信息缓存

特点:读多写少、一致性要求较高。

@Service
public class UserService {

    public User getUser(Long id) {
        String key = "user:info:" + id;
        Object value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return (User) value;
        }
        User user = userMapper.selectById(id);
        if (user != null) {
            redisTemplate.opsForValue().set(key, user, 1, TimeUnit.HOURS);
        } else {
            redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
        }
        return user;
    }

    public void updateUser(User user) {
        userMapper.updateById(user);
        redisTemplate.delete("user:info:" + user.getId());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 场景三:排行榜

特点:实时排序、高并发读。

@Service
public class RankService {

    public void addScore(String userId, double score) {
        redisTemplate.opsForZSet().add("rank:score", userId, score);
    }

    public Set<Object> getTop10() {
        return redisTemplate.opsForZSet().reverseRange("rank:score", 0, 9);
    }

    public Long getUserRank(String userId) {
        return redisTemplate.opsForZSet().reverseRank("rank:score", userId);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

策略:ZSet 实时排序,不缓存到本地,直接读 Redis。

# 场景四:验证码 / Token

特点:短生命周期、自动过期。

@Service
public class CodeService {

    public void sendCode(String phone) {
        String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000));
        redisTemplate.opsForValue().set("code:" + phone, code, 5, TimeUnit.MINUTES);
    }

    public boolean verifyCode(String phone, String code) {
        String cached = (String) redisTemplate.opsForValue().get("code:" + phone);
        if (code.equals(cached)) {
            redisTemplate.delete("code:" + phone);
            return true;
        }
        return false;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 场景五:字典数据 / 配置缓存

特点:数据量小、更新极少、访问频繁。

@Service
public class DictService {

    @Autowired
    private Cache<String, List<Dict>> dictCache;

    public List<Dict> getDictByType(String type) {
        return dictCache.get(type, k -> dictMapper.selectByType(k));
    }

    public void refreshDict(String type) {
        dictCache.invalidate(type);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

策略:本地缓存(Caffeine)即可,无需 Redis。

# 场景六:静态资源发布(前端 + CDN)

特点:前端构建产物,需全量缓存。

# Vite 构建配置
// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        // 文件名带 contenthash
        chunkFileNames: 'assets/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]',
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
# Nginx 静态资源缓存
location ~* \.(js|css|png|jpg|svg|woff2?)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    # CDN 会遵循此头缓存 1 年
}
1
2
3
4
5

策略:文件名带 hash + 浏览器 / CDN 缓存 1 年 + 发布新版本时 URL 变化自动失效。

# 缓存使用注意事项

# 1. 大 Key 问题

问题 说明
单个 Value 过大 如缓存一个 10MB 的列表,导致 Redis 阻塞、网络延迟
Hash / Set / List 元素过多 如一个 Set 存 100 万个元素

解决:拆分 Key,如 user:1:info、user:1:profile,单 Key 控制在 10KB 以内。

# 2. 热 Key 问题

问题 说明
单个 Key 访问量过大 如热点商品,单节点 Redis 承受不住

解决:

  • 本地缓存兜底(多级缓存)
  • Key 分散:hotkey:1、hotkey:2... 随机读其中一个

# 3. 缓存预热

系统启动时,提前加载热点数据到缓存,避免冷启动时大量请求打到数据库。

@PostConstruct
public void preloadCache() {
    List<Product> hotProducts = productMapper.selectHotProducts();
    for (Product product : hotProducts) {
        redisTemplate.opsForValue().set("product:" + product.getId(), product, 30, TimeUnit.MINUTES);
    }
}
1
2
3
4
5
6
7

# 4. 缓存监控

指标 说明
缓存命中率 低于 80% 需检查缓存策略
内存使用率 接近 maxmemory 需扩容或调整淘汰策略
QPS 监控缓存负载
大 Key / 热 Key 定期扫描排查
CDN 命中率 低于 90% 需检查缓存配置
Nginx 缓存命中率 通过 $upstream_cache_status 统计

# 5. 序列化选择

序列化方式 优点 缺点
JDK 序列化 兼容性好 体积大、不可读
JSON(Jackson) 体积小、可读 需要无参构造
Protobuf 体积最小、速度快 需定义 proto 文件

💡 推荐 GenericJackson2JsonRedisSerializer,可读且兼容性好。

# 总结

# 全链路缓存总览

层级 缓存介质 典型耗时 核心作用 失效方式
浏览器缓存 磁盘 / 内存 ~0ms 拦截静态资源请求 Cache-Control / ETag
CDN 缓存 边缘节点 5~20ms 就近返回静态资源 TTL / 主动刷新
Nginx 缓存 磁盘 1~5ms 拦截热点接口请求 TTL / purge
本地缓存 JVM 堆内存 纳秒级 拦截热点数据查询 TTL / LRU
分布式缓存 Redis 内存 1~2ms 拦截数据库查询 TTL / 主动删除
数据库缓存 Buffer Pool 微秒级 减少磁盘 I/O 自动管理

# 设计要点

要点 说明
全链路思维 缓存不只是 Redis,从浏览器到数据库每一层都有缓存
分层职责 浏览器/CDN 管静态资源,Nginx 管接口响应,应用层管业务数据
缓存选型 单机热点 → Caffeine;分布式共享 → Redis;高并发 → 多级缓存
更新策略 Cache Aside(先更新 DB 再删缓存)最常用
穿透 缓存空值 / 布隆过滤器
击穿 互斥锁 / 逻辑过期
雪崩 随机过期时间 / 多级缓存 / 高可用
淘汰策略 Redis 推荐 allkeys-lru
大 Key 拆分,单 Key < 10KB
热 Key 本地缓存兜底 / Key 分散
监控 各层命中率、内存、QPS

记忆口诀:浏览器管静态,CDN 管就近,Nginx 管接口,Caffeine 管热点,Redis 管数据,数据库管磁盘。穿透查不到(缓存空值),击穿热点过期(加锁),雪崩大面积过期(随机 TTL)。更新先 DB 后删缓存,多级缓存逐层兜底。

上次更新: 2026/8/21 00:25:55
过滤器、拦截器、AOP切面与其他拦截机制
UML基础入门

← 过滤器、拦截器、AOP切面与其他拦截机制 UML基础入门→

Theme by Vdoing | Copyright © 2023-2026 沉梦听雨 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式