轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • Vue.js
  • 前端工程化
  • 浏览器与Web API
  • 架构设计与模式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • Vue.js
  • 前端工程化
  • 浏览器与Web API
  • 架构设计与模式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 架构设计与模式

    • 高可用-分布式基础之CAP理论
    • 高可用-服务容错与降级策略
    • 高性能-缓存架构设计
      • 一、为什么需要缓存
        • 1、性能差距的本质
        • 2、缓存的核心价值
        • 3、缓存的适用场景
      • 二、缓存的基本原理
        • 1、缓存的读写策略
        • 1.1、Cache Aside(旁路缓存)
        • 1.2、Read Through(穿透读)
        • 1.3、Write Through(穿透写)
        • 1.4、Write Behind(异步写回)
        • 2、缓存更新策略对比
      • 三、多级缓存架构
        • 1、经典的三级缓存架构
        • 1.1、浏览器缓存(L1)
        • 1.2、CDN缓存(L2)
        • 1.3、应用服务器本地缓存(L3)
        • 1.4、分布式缓存(L4)
        • 2、多级缓存的协同策略
        • 2.1、缓存穿透策略
        • 2.2、缓存更新策略
        • 2.3、本地缓存同步方案
      • 四、缓存一致性问题
        • 1、数据不一致的场景分析
        • 1.1、先删缓存,再更新数据库
        • 1.2、先更新数据库,再删缓存
        • 2、一致性保证方案
        • 2.1、延迟双删(推荐)
        • 2.2、基于binlog的异步更新(最佳实践)
        • 2.3、设置合理的过期时间
        • 2.4、读写锁方案
        • 3、一致性方案选择
      • 五、缓存典型问题及解决方案
        • 1、缓存穿透
        • 解决方案1: 缓存空值
        • 解决方案2: 布隆过滤器(推荐)
        • 2、缓存击穿
        • 解决方案1: 互斥锁
        • 解决方案2: 热点数据永不过期
        • 解决方案3: 随机过期时间
        • 3、缓存雪崩
        • 解决方案1: 高可用架构
        • 解决方案2: 过期时间打散
        • 解决方案3: 多级缓存
        • 解决方案4: 限流降级
        • 4、缓存问题总结
      • 六、缓存实战最佳实践
        • 1、缓存Key设计规范
        • 1.1、命名规范
        • 1.2、长度控制
        • 1.3、避免特殊字符
        • 2、缓存值设计规范
        • 2.1、数据序列化选择
        • 2.2、大对象拆分
        • 2.3、使用Hash优化小对象存储
        • 3、过期策略设计
        • 3.1、TTL设置原则
        • 3.2、主动刷新策略
        • 4、监控与告警
        • 4.1、关键指标监控
        • 4.2、Redis慢查询监控
        • 5、缓存容量规划
        • 5.1、容量估算公式
        • 5.2、淘汰策略选择
      • 七、缓存架构演进案例
        • 案例:电商商品详情页优化
        • 阶段1:无缓存(初期)
        • 阶段2:引入Redis单机缓存
        • 阶段3:本地缓存+Redis两级缓存
        • 阶段4:CDN+多级缓存
      • 八、总结与建议
        • 1、核心要点回顾
        • 2、缓存设计检查清单
        • 3、学习进阶路线
    • 高性能-性能优化方法论
  • 代码质量管理

  • 基础

  • 操作系统

  • 计算机网络

  • AI

  • 编程范式

  • 安全

  • 中间件

  • 心得

  • 架构
  • 架构设计与模式
轩辕李
2024-06-06
目录

高性能-缓存架构设计

在互联网应用中,性能优化永远是一个绕不开的话题。当用户量增长、数据规模扩大时,单纯依赖数据库查询很快就会遇到性能瓶颈。这时候,缓存就成为了提升系统性能最有效的手段之一。

根据统计,合理使用缓存可以将系统响应时间从秒级降低到毫秒级,QPS提升10-100倍。但缓存并非简单的"能用就用",不当的缓存设计反而会引入数据一致性问题、缓存雪崩等严重故障。

本文将深入探讨缓存架构设计的方方面面,从基础原理到实战经验,帮助你构建高性能、高可靠的缓存系统。

# 一、为什么需要缓存

# 1、性能差距的本质

要理解缓存的价值,首先要认识不同存储介质的性能差异:

存储类型 延迟 吞吐量 成本 持久性
CPU L1缓存 0.5ns 极高 极高 否
CPU L2缓存 7ns 很高 很高 否
内存 100ns 高 中 否
SSD 0.1-0.2ms 中 中低 是
机械硬盘 10ms 低 低 是
网络请求 0.5-100ms 低 - -

从表中可以看出,内存访问速度比磁盘快10万倍,这就是缓存能够大幅提升性能的根本原因。

# 2、缓存的核心价值

减少数据库压力:

  • 数据库连接数有限(如MySQL默认151个连接)
  • 磁盘I/O是瓶颈,大量查询会导致慢查询堆积
  • 缓存可以拦截80-90%的读请求

提升响应速度:

  • 数据库查询:10-100ms
  • Redis缓存查询:1-5ms
  • 本地缓存查询:0.1ms以下

降低成本:

  • 通过缓存减少数据库实例数量
  • 避免频繁的磁盘I/O损耗
  • 减少网络带宽消耗

应对高并发:

  • 单个Redis实例可支持10万QPS
  • 数据库单表很难承受1万QPS

# 3、缓存的适用场景

适合使用缓存:

  • 读多写少的数据(如商品信息、用户资料)
  • 计算成本高的结果(如推荐算法、统计报表)
  • 访问频繁的热点数据(如首页数据、排行榜)
  • 对实时性要求不高的数据(可接受秒级延迟)

不适合使用缓存:

  • 写多读少的数据(如日志、监控数据)
  • 强一致性要求(如金融交易、库存扣减)
  • 数据量小且查询快(缓存反而增加复杂度)
  • 访问模式分散(缓存命中率低)

# 二、缓存的基本原理

# 1、缓存的读写策略

# 1.1、Cache Aside(旁路缓存)

这是最常用的缓存模式,应用程序直接与缓存和数据库交互:

读流程:

1. 应用程序先查询缓存
2. 缓存命中则直接返回
3. 缓存未命中则查询数据库
4. 将数据库结果写入缓存
5. 返回结果

写流程:

1. 应用程序先更新数据库
2. 删除缓存(或更新缓存)

优点:

  • 简单易实现,业务逻辑清晰
  • 缓存故障不影响数据库访问
  • 适合读多写少场景

缺点:

  • 首次访问必定缓存miss
  • 存在短暂的数据不一致窗口

实现示例:

public User getUser(Long userId) {
    // 1. 先查缓存
    User user = redis.get("user:" + userId);
    if (user != null) {
        return user;
    }
    
    // 2. 缓存未命中,查询数据库
    user = userDao.selectById(userId);
    if (user != null) {
        // 3. 写入缓存,设置过期时间
        redis.setex("user:" + userId, 3600, user);
    }
    
    return user;
}

public void updateUser(User user) {
    // 1. 先更新数据库
    userDao.updateById(user);
    
    // 2. 删除缓存
    redis.del("user:" + user.getId());
}

# 1.2、Read Through(穿透读)

缓存层作为数据访问的统一入口,应用程序只与缓存交互:

应用程序 -> 缓存层 -> 数据库

特点:

  • 缓存未命中时,由缓存层自动加载数据
  • 对应用程序透明,简化业务代码
  • 典型实现:Spring Cache、Guava Cache

示例:

@Cacheable(value = "users", key = "#userId")
public User getUser(Long userId) {
    return userDao.selectById(userId);
}

# 1.3、Write Through(穿透写)

写操作也由缓存层代理:

应用程序 -> 缓存层 -> 同时更新缓存和数据库

特点:

  • 保证缓存和数据库的强一致性
  • 写操作变慢(需要同步更新两处)
  • 适合强一致性场景

# 1.4、Write Behind(异步写回)

也称为Write Back,缓存层异步批量写入数据库:

应用程序 -> 缓存(立即返回) -> 异步批量写入数据库

优点:

  • 写性能极高
  • 可以批量合并写操作
  • 适合写密集型场景

缺点:

  • 可能丢失数据(缓存宕机)
  • 数据库和缓存可能不一致
  • 实现复杂

应用场景:

  • 浏览量、点赞数等统计计数
  • 用户行为日志
  • 对数据丢失不敏感的场景

# 2、缓存更新策略对比

策略 一致性 性能 复杂度 适用场景
Cache Aside 弱一致 高 低 通用场景
Read Through 弱一致 高 中 简化读逻辑
Write Through 强一致 中 中 强一致性要求
Write Behind 最终一致 极高 高 高并发写场景

# 三、多级缓存架构

在实际生产环境中,单一缓存层往往无法满足所有需求。通过构建多级缓存,可以进一步提升性能和可用性。

# 1、经典的三级缓存架构

请求 -> 浏览器缓存 -> CDN缓存 -> 应用服务器缓存 -> Redis缓存 -> 数据库

# 1.1、浏览器缓存(L1)

作用:

  • 静态资源(JS/CSS/图片)本地存储
  • 减少网络请求,提升页面加载速度

实现方式:

  • HTTP缓存头:Cache-Control、ETag、Last-Modified
  • LocalStorage、SessionStorage
  • IndexedDB

适用数据:

  • 静态资源文件
  • 不常变化的配置数据
  • 用户个人偏好设置

# 1.2、CDN缓存(L2)

作用:

  • 内容分发网络,将资源缓存到离用户最近的节点
  • 减轻源站压力,加速全球访问

适用数据:

  • 静态资源(图片、视频、CSS、JS)
  • API响应(设置短过期时间)
  • 下载文件

关键配置:

# Nginx CDN缓存配置示例
location ~* \.(jpg|jpeg|png|gif|css|js)$ {
    expires 7d;
    add_header Cache-Control "public, immutable";
}

# 1.3、应用服务器本地缓存(L3)

作用:

  • 进程内缓存,访问速度最快
  • 减少网络开销和Redis压力

常用实现:

  • Caffeine(Java): 高性能本地缓存
  • Guava Cache(Java): Google出品
  • lru-cache(Node.js)
  • cachetools(Python)

示例代码:

// Caffeine缓存示例
LoadingCache<Long, User> userCache = Caffeine.newBuilder()
    .maximumSize(10000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(userId -> userDao.selectById(userId));

// 使用
User user = userCache.get(userId);

优缺点:

优点 缺点
访问速度极快(微秒级) 容量受限于JVM内存
无网络开销 多实例间数据不一致
减轻Redis压力 更新困难(需要通知所有实例)

# 1.4、分布式缓存(L4)

主流选择: Redis

核心优势:

  • 性能极高(单实例10万QPS)
  • 数据结构丰富(String/Hash/List/Set/ZSet)
  • 支持持久化(RDB/AOF)
  • 支持集群和高可用

数据类型应用场景:

数据类型 应用场景 示例
String 简单KV存储、计数器 用户信息、访问量
Hash 对象存储 用户详情、商品信息
List 队列、时间线 消息队列、评论列表
Set 去重、交集运算 标签、共同好友
ZSet 排行榜、延时队列 热度排行、定时任务

Redis缓存示例:

// String类型:用户基本信息
redis.setex("user:1001", 3600, JSON.toJSONString(user));

// Hash类型:用户详情
redis.hset("user:1001:detail", "name", "张三");
redis.hset("user:1001:detail", "age", "25");

// ZSet类型:商品热度排行
redis.zadd("product:hot:rank", score, productId);
List<String> topProducts = redis.zrevrange("product:hot:rank", 0, 9);

# 2、多级缓存的协同策略

# 2.1、缓存穿透策略

逐层查询:

public User getUser(Long userId) {
    // L1: 本地缓存
    User user = localCache.get(userId);
    if (user != null) return user;
    
    // L2: Redis缓存
    user = redis.get("user:" + userId);
    if (user != null) {
        localCache.put(userId, user);
        return user;
    }
    
    // L3: 数据库
    user = userDao.selectById(userId);
    if (user != null) {
        redis.setex("user:" + userId, 3600, user);
        localCache.put(userId, user);
    }
    
    return user;
}

# 2.2、缓存更新策略

数据更新时需要失效所有层级的缓存:

public void updateUser(User user) {
    // 1. 更新数据库
    userDao.updateById(user);
    
    // 2. 删除Redis缓存
    redis.del("user:" + user.getId());
    
    // 3. 删除本地缓存
    localCache.invalidate(user.getId());
    
    // 4. 通知其他实例删除本地缓存(通过Redis Pub/Sub)
    redis.publish("cache:invalidate:user", user.getId());
}

# 2.3、本地缓存同步方案

方案一: Redis Pub/Sub

// 订阅缓存失效消息
redis.subscribe("cache:invalidate:*", (channel, message) -> {
    if (channel.equals("cache:invalidate:user")) {
        localCache.invalidate(Long.parseLong(message));
    }
});

方案二: Canal监听MySQL Binlog

  • 实时监听数据库变更
  • 自动失效对应的缓存
  • 对业务代码无侵入

方案三: 短过期时间

  • 本地缓存设置较短的TTL(如1-5分钟)
  • 容忍短暂的数据不一致
  • 实现简单,适合大部分场景

# 四、缓存一致性问题

缓存一致性是缓存架构设计中最核心也是最棘手的问题。

# 1、数据不一致的场景分析

# 1.1、先删缓存,再更新数据库

时间线:
T1: 线程A删除缓存
T2: 线程B查询缓存(miss)
T3: 线程B查询数据库(旧数据)
T4: 线程A更新数据库
T5: 线程B将旧数据写入缓存

结果: 缓存中是旧数据

# 1.2、先更新数据库,再删缓存

时间线:
T1: 线程A查询缓存(miss)
T2: 线程A查询数据库(旧数据)
T3: 线程B更新数据库
T4: 线程B删除缓存
T5: 线程A将旧数据写入缓存

结果: 缓存中还是旧数据

但概率极低: T3-T4(数据库更新+缓存删除)通常在毫秒级,而T1-T2(查询数据库)可能需要几十毫秒,T5很难插入这个时间窗口。

# 2、一致性保证方案

# 2.1、延迟双删(推荐)

策略: 先删缓存 -> 更新数据库 -> 延迟N毫秒 -> 再删缓存

public void updateUser(User user) {
    String cacheKey = "user:" + user.getId();
    
    // 1. 删除缓存
    redis.del(cacheKey);
    
    // 2. 更新数据库
    userDao.updateById(user);
    
    // 3. 延迟删除缓存(异步执行)
    CompletableFuture.delayedExecutor(500, TimeUnit.MILLISECONDS)
        .execute(() -> redis.del(cacheKey));
}

延迟时间如何确定:

  • 应大于主从复制延迟(通常100-500ms)
  • 应大于一次数据库查询的时间
  • 建议设置为500ms-1s

优点:

  • 能够覆盖绝大多数不一致场景
  • 实现简单

缺点:

  • 延迟期间可能读到旧数据
  • 无法100%保证一致性

# 2.2、基于binlog的异步更新(最佳实践)

使用Canal监听MySQL的binlog:

MySQL -> binlog -> Canal -> 消息队列 -> 缓存更新服务 -> 删除/更新Redis

优点:

  • 对业务代码无侵入
  • 近实时同步(延迟<1s)
  • 可靠性高(消息队列保证)

实现方案:

// Canal监听器
@CanalEventListener
public class UserCacheListener {
    
    @ListenPoint(schema = "test", table = "user")
    public void onUserUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
        if (eventType == CanalEntry.EventType.UPDATE 
            || eventType == CanalEntry.EventType.DELETE) {
            
            String userId = rowData.getAfterColumns("id");
            redis.del("user:" + userId);
        }
    }
}

# 2.3、设置合理的过期时间

兜底策略: 即使缓存和数据库不一致,也会在TTL后自动修复

// 设置1小时过期
redis.setex("user:" + userId, 3600, user);

过期时间设置建议:

数据类型 推荐TTL 原因
热点数据 1-6小时 访问频繁,命中率高
普通数据 30分钟-1小时 平衡命中率和一致性
实时性要求高 1-5分钟 快速过期,减少不一致窗口
计算成本高 12-24小时 减少重复计算

# 2.4、读写锁方案

适用场景: 强一致性要求(如库存扣减)

public void updateUserWithLock(User user) {
    String lockKey = "lock:user:" + user.getId();
    String cacheKey = "user:" + user.getId();
    
    // 获取分布式锁
    RLock lock = redisson.getLock(lockKey);
    try {
        lock.lock(5, TimeUnit.SECONDS);
        
        // 更新数据库
        userDao.updateById(user);
        
        // 删除缓存
        redis.del(cacheKey);
        
    } finally {
        lock.unlock();
    }
}

缺点: 性能下降,不适合高并发场景

# 3、一致性方案选择

场景 推荐方案 理由
一般业务 延迟双删 + TTL 简单有效,99%场景够用
核心业务 Canal + 延迟双删 双重保险,可靠性高
强一致性 读写锁 牺牲性能保证一致性
最终一致 仅设置TTL 实现最简单

# 五、缓存典型问题及解决方案

# 1、缓存穿透

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

场景:

  • 恶意攻击:故意查询不存在的ID
  • 业务逻辑缺陷:前端传入非法参数

危害:

  • 数据库压力骤增
  • 缓存失去保护作用
  • 可能导致数据库崩溃

# 解决方案1: 缓存空值

public User getUser(Long userId) {
    String cacheKey = "user:" + userId;
    
    // 查询缓存
    String cachedValue = redis.get(cacheKey);
    if (cachedValue != null) {
        if ("NULL".equals(cachedValue)) {
            return null;  // 之前查询过,数据不存在
        }
        return JSON.parseObject(cachedValue, User.class);
    }
    
    // 查询数据库
    User user = userDao.selectById(userId);
    if (user != null) {
        redis.setex(cacheKey, 3600, JSON.toJSONString(user));
    } else {
        // 缓存空值,设置较短的过期时间
        redis.setex(cacheKey, 60, "NULL");
    }
    
    return user;
}

注意事项:

  • 空值TTL要短(避免正常新增数据后查不到)
  • 需要考虑内存占用

# 解决方案2: 布隆过滤器(推荐)

原理: 用极小的内存判断元素是否存在(允许一定误判率)

// 初始化布隆过滤器(预计100万条数据,1%误判率)
BloomFilter<Long> userBloomFilter = BloomFilter.create(
    Funnels.longFunnel(), 
    1000000, 
    0.01
);

// 启动时加载所有用户ID
List<Long> userIds = userDao.selectAllIds();
userIds.forEach(userBloomFilter::put);

// 查询时先判断
public User getUser(Long userId) {
    // 布隆过滤器判断
    if (!userBloomFilter.mightContain(userId)) {
        return null;  // 一定不存在
    }
    
    // 后续缓存和数据库查询逻辑
    // ...
}

Redis版本的布隆过滤器:

// 使用Redisson的布隆过滤器
RBloomFilter<Long> bloomFilter = redisson.getBloomFilter("user:bloom");
bloomFilter.tryInit(1000000L, 0.01);

// 新增用户时添加
bloomFilter.add(userId);

// 查询时判断
if (!bloomFilter.contains(userId)) {
    return null;
}

优缺点对比:

方案 优点 缺点 适用场景
缓存空值 实现简单 占用内存,需要更新 数据量小,攻击少
布隆过滤器 内存极省,性能高 存在误判,不支持删除 数据量大,读多写少

# 2、缓存击穿

定义: 某个热点key在失效瞬间,大量并发请求同时打到数据库。

场景:

  • 热门商品缓存过期
  • 明星微博缓存失效
  • 热点新闻缓存过期

危害:

  • 数据库瞬时压力过大
  • 可能导致慢查询堆积
  • 影响其他正常查询

# 解决方案1: 互斥锁

public User getUser(Long userId) {
    String cacheKey = "user:" + userId;
    String lockKey = "lock:user:" + userId;
    
    // 查询缓存
    User user = redis.get(cacheKey);
    if (user != null) {
        return user;
    }
    
    // 尝试获取锁
    RLock lock = redisson.getLock(lockKey);
    try {
        // 等待最多100ms,锁定最多10s
        if (lock.tryLock(100, 10000, TimeUnit.MILLISECONDS)) {
            // 再次检查缓存(可能其他线程已经加载了)
            user = redis.get(cacheKey);
            if (user != null) {
                return user;
            }
            
            // 查询数据库
            user = userDao.selectById(userId);
            if (user != null) {
                redis.setex(cacheKey, 3600, user);
            }
            
            return user;
        } else {
            // 获取锁失败,等待50ms后重试
            Thread.sleep(50);
            return getUser(userId);
        }
    } finally {
        lock.unlock();
    }
}

优点: 保证同一时刻只有一个请求查询数据库 缺点: 增加了等待时间,降低了并发性能

# 解决方案2: 热点数据永不过期

逻辑过期:

@Data
class CacheData {
    private Object data;          // 实际数据
    private Long expireTime;      // 逻辑过期时间
}

public User getUser(Long userId) {
    String cacheKey = "user:" + userId;
    
    CacheData cacheData = redis.get(cacheKey);
    
    // 缓存未命中
    if (cacheData == null) {
        // 加载数据(同样需要加锁)
        return loadUserWithLock(userId);
    }
    
    User user = (User) cacheData.getData();
    
    // 判断是否逻辑过期
    if (System.currentTimeMillis() > cacheData.getExpireTime()) {
        // 异步更新缓存
        CompletableFuture.runAsync(() -> {
            loadUserWithLock(userId);
        });
    }
    
    // 返回旧数据
    return user;
}

优点:

  • 始终能返回数据(即使是旧数据)
  • 不会有大量请求打到数据库

缺点:

  • 可能读到过期数据
  • 需要额外的后台线程更新

# 解决方案3: 随机过期时间

// 避免大量缓存同时过期
int ttl = 3600 + new Random().nextInt(300);  // 3600-3900秒
redis.setex(cacheKey, ttl, user);

# 3、缓存雪崩

定义: 大量缓存在同一时间失效,导致所有请求都打到数据库。

触发场景:

  • Redis宕机
  • 大量缓存同时过期
  • 缓存服务重启

危害:

  • 数据库瞬间压力暴增
  • 可能引发连锁故障
  • 整个系统不可用

# 解决方案1: 高可用架构

Redis Sentinel(哨兵模式):

Master节点 <- Sentinel监控 
  |
Slave1, Slave2(自动故障转移)

Redis Cluster(集群模式):

16384个哈希槽分布在多个Master节点
每个Master有多个Slave副本

# 解决方案2: 过期时间打散

// 基础TTL + 随机值
int baseTtl = 3600;
int randomTtl = new Random().nextInt(600);  // 0-600秒
redis.setex(cacheKey, baseTtl + randomTtl, data);

# 解决方案3: 多级缓存

public User getUser(Long userId) {
    // L1: 本地缓存(即使Redis宕机也能提供服务)
    User user = localCache.get(userId);
    if (user != null) return user;
    
    // L2: Redis缓存
    try {
        user = redis.get("user:" + userId);
        if (user != null) {
            localCache.put(userId, user);
            return user;
        }
    } catch (Exception e) {
        // Redis异常,降级到数据库(有本地缓存保护)
        log.error("Redis error", e);
    }
    
    // L3: 数据库(有限流保护)
    user = getUserFromDbWithRateLimit(userId);
    return user;
}

# 解决方案4: 限流降级

// 使用Guava RateLimiter限流
RateLimiter dbLimiter = RateLimiter.create(1000);  // 每秒1000次

public User getUserFromDb(Long userId) {
    if (!dbLimiter.tryAcquire()) {
        throw new ServiceException("系统繁忙,请稍后重试");
    }
    return userDao.selectById(userId);
}

Sentinel熔断降级:

@SentinelResource(value = "getUser", 
    fallback = "getUserFallback",
    blockHandler = "getUserBlockHandler")
public User getUser(Long userId) {
    return userDao.selectById(userId);
}

// 降级逻辑:返回默认值
public User getUserFallback(Long userId, Throwable e) {
    return User.builder().id(userId).name("默认用户").build();
}

# 4、缓存问题总结

问题 原因 影响 解决方案
穿透 查询不存在的数据 DB压力大 布隆过滤器/缓存空值
击穿 热点key过期 DB瞬时压力大 互斥锁/永不过期
雪崩 大量key同时失效 系统整体不可用 高可用/过期打散/限流

# 六、缓存实战最佳实践

# 1、缓存Key设计规范

# 1.1、命名规范

推荐格式: 业务模块:对象类型:对象ID[:属性]

user:info:1001                  # 用户基本信息
user:session:1001               # 用户会话
product:detail:2001             # 商品详情
product:inventory:2001          # 商品库存
order:list:1001:page:1          # 订单列表第1页

优点:

  • 可读性强,便于调试
  • 便于批量操作(keys user:*)
  • 避免命名冲突

# 1.2、长度控制

问题: Redis的key长度影响内存和性能

建议:

  • 尽量控制在50字符以内
  • 对于超长key可以使用hash
// 不推荐:key过长
redis.set("user:info:detail:all:fields:1001", data);

// 推荐:使用hash
redis.hset("user:detail:1001", "all_fields", data);

# 1.3、避免特殊字符

禁用字符: 空格、换行符、特殊控制字符

// 错误
redis.set("user name:1001", data);

// 正确
redis.set("user:name:1001", data);

# 2、缓存值设计规范

# 2.1、数据序列化选择

序列化方式 优点 缺点 适用场景
JSON 可读性好,通用性强 体积较大 调试阶段,跨语言
Protobuf 体积小,速度快 需要定义schema 性能要求高
Hessian Java友好 跨语言支持差 Java技术栈
Kryo 性能极高 需要注册类 高性能场景

推荐: 生产环境使用Protobuf或Kryo,开发调试使用JSON

# 2.2、大对象拆分

问题: 单个key过大(>1MB)会导致性能问题

解决方案:

// 不推荐:存储整个用户对象
User user = new User();
user.setBasicInfo(...);
user.setExtendInfo(...);
user.setOrderList(...);  // 可能很大
redis.set("user:1001", user);

// 推荐:拆分存储
redis.hset("user:1001", "basic", user.getBasicInfo());
redis.hset("user:1001", "extend", user.getExtendInfo());
redis.set("user:1001:orders", user.getOrderList());

# 2.3、使用Hash优化小对象存储

场景: 大量小对象(如100万个用户的基本信息)

// 方案1:每个用户一个key(不推荐)
redis.set("user:1", "{name:张三,age:20}");
redis.set("user:2", "{name:李四,age:25}");
// 100万个key,内存占用大

// 方案2:使用hash分桶(推荐)
// 用户1->bucket_1, 用户2->bucket_2, ...
long bucket = userId % 1000;
redis.hset("user:bucket:" + bucket, String.valueOf(userId), userData);

优化效果: 内存占用减少30-50%

# 3、过期策略设计

# 3.1、TTL设置原则

// 根据数据特性设置不同TTL
public enum CacheTTL {
    HOT_DATA(6 * 3600),        // 热点数据6小时
    NORMAL_DATA(1 * 3600),     // 普通数据1小时  
    REALTIME_DATA(5 * 60),     // 实时数据5分钟
    EXPENSIVE_DATA(24 * 3600); // 计算昂贵的数据24小时
    
    private final int seconds;
}

# 3.2、主动刷新策略

定时任务预热热点数据:

@Scheduled(cron = "0 0 * * * ?")  // 每小时执行
public void warmUpCache() {
    // 查询热门商品(如销量TOP100)
    List<Product> hotProducts = productDao.selectHotProducts(100);
    
    for (Product product : hotProducts) {
        String cacheKey = "product:" + product.getId();
        redis.setex(cacheKey, 7200, product);
    }
}

# 4、监控与告警

# 4.1、关键指标监控

// 缓存命中率监控
public class CacheMetrics {
    private AtomicLong hits = new AtomicLong(0);
    private AtomicLong misses = new AtomicLong(0);
    
    public double getHitRate() {
        long totalHits = hits.get();
        long totalMisses = misses.get();
        long total = totalHits + totalMisses;
        
        return total == 0 ? 0 : (double) totalHits / total;
    }
    
    public void recordHit() {
        hits.incrementAndGet();
    }
    
    public void recordMiss() {
        misses.incrementAndGet();
    }
}

监控指标:

  • 命中率: 应该>80%,低于70%需要优化
  • 平均响应时间: Redis应<5ms
  • 内存使用率: 建议<75%,超过85%需扩容
  • 连接数: 避免连接池耗尽
  • 慢查询: 监控慢查询日志

# 4.2、Redis慢查询监控

# 配置慢查询(大于10ms记录)
config set slowlog-log-slower-than 10000

# 查看慢查询日志
slowlog get 10
// Java代码监控慢查询
public void checkSlowLog() {
    List<Slowlog> slowlogs = redis.slowlogGet(10);
    for (Slowlog log : slowlogs) {
        if (log.getExecutionTime() > 10000) {  // 大于10ms
            log.warn("慢查询: {}, 耗时: {}ms", 
                log.getArgs(), log.getExecutionTime() / 1000);
        }
    }
}

# 5、缓存容量规划

# 5.1、容量估算公式

总内存 = 数据量 × 单条数据大小 × (1 + 冗余系数)

冗余系数说明:
- Redis自身内存开销:约20-30%
- 内存碎片:约10-20%
- 建议冗余系数:1.5-2.0

示例:

场景:1000万用户,每个用户信息1KB
计算:10000000 × 1KB × 2 = 20GB

建议配置:32GB内存的Redis实例

# 5.2、淘汰策略选择

Redis的8种淘汰策略:

策略 说明 适用场景
noeviction 不淘汰,内存满时返回错误 不允许数据丢失
allkeys-lru 所有key中淘汰最少使用的 通用场景(推荐)
allkeys-lfu 所有key中淘汰访问频率最低的 有明显热点
allkeys-random 随机淘汰 无特定访问模式
volatile-lru 有过期时间的key中LRU 部分数据永久缓存
volatile-lfu 有过期时间的key中LFU 结合过期时间
volatile-ttl 优先淘汰TTL短的 按重要性设置TTL
volatile-random 随机淘汰有过期时间的 简单场景

配置建议:

# 生产环境推荐
maxmemory-policy allkeys-lru

# 七、缓存架构演进案例

# 案例:电商商品详情页优化

# 阶段1:无缓存(初期)

请求 -> 应用服务器 -> MySQL
响应时间:200ms
QPS上限:500

问题: 双11流量峰值达到5万QPS,数据库扛不住

# 阶段2:引入Redis单机缓存

请求 -> 应用服务器 -> Redis -> MySQL
响应时间:20ms(命中缓存) / 200ms(未命中)
QPS上限:5000
缓存命中率:60%

改进: 性能提升10倍,但命中率不理想

优化措施:

  • 预热热门商品
  • 调整TTL为2小时
  • 命中率提升到85%

# 阶段3:本地缓存+Redis两级缓存

请求 -> 本地Caffeine缓存 -> Redis -> MySQL
响应时间:1ms(本地) / 20ms(Redis) / 200ms(DB)
QPS上限:5万
本地缓存命中率:40%
Redis缓存命中率:50%
总命中率:70%

问题: 本地缓存更新不及时,有时读到旧数据

解决:

// 使用Redis Pub/Sub同步缓存失效
redis.subscribe("product:update", message -> {
    localCache.invalidate(message);
});

// 商品更新时发布消息
public void updateProduct(Product product) {
    productDao.update(product);
    redis.del("product:" + product.getId());
    redis.publish("product:update", product.getId());
}

# 阶段4:CDN+多级缓存

请求 -> CDN(静态资源) -> Nginx缓存 -> 本地缓存 -> Redis -> MySQL

静态资源(图片/CSS/JS): CDN命中率95%
商品详情HTML: Nginx缓存5分钟
动态数据: 本地缓存+Redis

最终效果:

  • 响应时间:平均5ms
  • QPS:单机支持10万
  • 总缓存命中率:92%
  • 数据库QPS:从5万降低到4000

# 八、总结与建议

# 1、核心要点回顾

缓存设计三原则:

  1. 合适的场景: 读多写少、允许一定延迟
  2. 多级架构: 浏览器->CDN->本地->Redis->DB
  3. 兜底策略: 限流、降级、熔断

一致性保障:

  • 优先选择: 延迟双删 + TTL
  • 核心业务: Canal监听binlog
  • 强一致性: 分布式锁(牺牲性能)

三大问题防范:

  • 穿透: 布隆过滤器
  • 击穿: 互斥锁 + 永不过期
  • 雪崩: 高可用 + 过期打散 + 限流

# 2、缓存设计检查清单

功能层面:

  • [ ] 是否选择了正确的缓存策略(Cache Aside/Read Through等)
  • [ ] 是否设计了合理的多级缓存架构
  • [ ] 是否考虑了缓存一致性问题
  • [ ] 是否处理了缓存穿透/击穿/雪崩

性能层面:

  • [ ] 缓存命中率是否>80%
  • [ ] 是否对热点数据做了预热
  • [ ] 是否设置了合理的过期时间
  • [ ] 是否避免了大key和热key

可靠性层面:

  • [ ] 是否配置了Redis高可用(Sentinel/Cluster)
  • [ ] 是否有缓存降级和限流策略
  • [ ] 是否有完善的监控和告警
  • [ ] 是否定期备份重要缓存数据

运维层面:

  • [ ] 是否有容量规划和扩容预案
  • [ ] 是否监控了慢查询和内存使用
  • [ ] 是否有缓存预热和数据迁移方案
  • [ ] 是否有故障演练和应急预案

# 3、学习进阶路线

基础阶段:

  1. 掌握Redis基本数据类型和命令
  2. 理解缓存读写策略
  3. 学习Spring Cache等缓存框架

进阶阶段:

  1. 深入学习Redis集群架构
  2. 研究缓存一致性解决方案
  3. 实践多级缓存架构

高级阶段:

  1. 阅读Redis源码(了解内存模型、持久化)
  2. 学习分布式缓存设计模式
  3. 研究大厂缓存架构实践

推荐资源:

  • 书籍:
    • 《Redis设计与实现》 (opens new window) - 黄健宏著,深入剖析Redis内部机制
    • 《Redis深度历险》 (opens new window) - 钱文品著,偏实战应用
    • 《Redis开发与运维》 (opens new window) - 付磊等著,全面的运维指南
  • 开源项目:
    • Redisson (opens new window) - 强大的Redis Java客户端
    • Lettuce (opens new window) - 高性能异步Redis客户端
    • Spring Cache (opens new window) - Spring官方缓存抽象
    • Caffeine (opens new window) - 高性能本地缓存库
  • 官方文档:
    • Redis官方文档 (opens new window)
    • Redis中文网 (opens new window)
  • 实战案例:
    • 美团技术博客 - 缓存专题 (opens new window)
    • 阿里云Redis最佳实践 (opens new window)

缓存是一门平衡的艺术,需要在性能、一致性、复杂度之间做权衡。没有完美的方案,只有最适合当前业务场景的方案。希望本文能帮助你构建高性能、高可靠的缓存架构。

祝你变得更强!

编辑 (opens new window)
#缓存#Redis#性能优化
上次更新: 2025/12/06
高可用-服务容错与降级策略
高性能-性能优化方法论

← 高可用-服务容错与降级策略 高性能-性能优化方法论→

最近更新
01
AI编程时代的一些心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code 最佳实践(个人版)
08-01
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式