1. 缓存基础概念:什么是缓存、缓存的作用与价值、缓存穿透/击穿/雪崩的定义

1.1 什么是缓存

缓存(Cache)是一种将数据存储在临时存储介质中的技术,目的是在后续请求中能够更快地访问这些数据,而无需重新计算或从原始数据源(如数据库、磁盘文件、远程服务)重新获取。在计算机系统中,缓存无处不在:从 CPU 的 L1/L2/L3 缓存、操作系统的页缓存,到浏览器缓存、CDN 缓存,再到应用层的 Redis、Memcached,都是缓存思想的体现。

在 Spring Boot 微服务架构中,缓存通常指应用层缓存,即使用内存数据库(如 Redis)或本地内存(如 Caffeine)暂存热点数据,以减少对后端数据库或外部服务的访问压力。

核心公式: 缓存命中率 = 缓存命中的请求数 / 总请求数。命中率越高,缓存带来的性能收益越大。

1.2 缓存的作用与价值

缓存的价值可以从三个维度衡量:

维度 说明 典型收益
性能 减少数据访问延迟,提升系统响应速度 读操作延迟从 10~50ms(DB)降至 1~5ms(缓存)
吞吐量 降低后端系统负载,支持更高并发 数据库 QPS 从 1000 提升至 10000+
成本 减少对昂贵资源(如数据库连接、计算资源)的消耗 同等硬件下支撑更多业务请求

在 Spring Boot 项目中,缓存最常见的应用场景包括:

  • 热点数据查询:如用户信息、商品详情、配置参数等频繁读取但变化不频繁的数据。
  • 复杂计算结果:如报表统计、推荐算法结果,避免重复计算。
  • 会话状态:使用 Redis 缓存用户 Session,实现分布式会话共享。
  • 限流与防重:利用缓存原子操作实现接口限流计数器或幂等令牌。

1.3 缓存穿透 / 击穿 / 雪崩的定义

这三个概念是缓存系统中最高频的故障模式,理解它们的区别是设计缓存一致性方案的前提。

故障模式 定义 触发条件 影响范围
缓存穿透 请求查询一个缓存和数据库中都不存在的数据,导致每次请求都穿透缓存直达数据库 恶意攻击或查询不存在的 key(如 -1、空字符串) 数据库压力骤增,可能引发宕机
缓存击穿 一个热点 key 在缓存失效的瞬间,大量并发请求同时访问该 key,全部穿透到数据库 热点 key 过期 + 高并发访问 数据库单点压力暴增,可能引发连锁故障
缓存雪崩 大量缓存 key 在同一时间集中过期失效,导致所有请求全部落到数据库 缓存设置相同的过期时间、缓存服务宕机 数据库被瞬间打垮,系统整体不可用
常见误区: 缓存穿透和缓存击穿容易混淆。穿透是"数据不存在"导致的持续穿透;击穿是"数据存在但 key 刚好过期"导致的瞬时穿透。穿透是"一直不存在",击穿是"曾经存在但刚过期"。

1.4 三种故障模式的对比流程图

下图展示了缓存穿透、击穿、雪崩在请求链路中的不同表现:

缓存穿透 / 击穿 / 雪崩 对比流程图 客户端请求 缓存层 (Redis) 数据库 (DB) 缓存穿透 请求 key 不存在 缓存未命中 → 数据库未命中 每次请求都穿透到 DB 缓存击穿 热点 key 刚好过期 大量并发请求同时穿透 数据库单点压力暴增 缓存雪崩 大量 key 同时过期 所有请求全部落到 DB 数据库被瞬间打垮 常见解决方案 缓存穿透 ① 缓存空值(null) ② 布隆过滤器前置拦截 ③ 参数校验与限流 缓存击穿 ① 互斥锁(Mutex Key) ② 逻辑过期 + 异步刷新 ③ 热点 key 永不过期 缓存雪崩 ① 过期时间加随机值 ② 缓存集群高可用 ③ 多级缓存(本地+远程)

1.5 缓存穿透的代码示例

以下是一个典型的缓存穿透场景及空值缓存解决方案的伪代码:

// 缓存穿透:查询不存在的用户
public User getUserById(Long id) {
    // 1. 尝试从缓存获取
    User user = redisTemplate.opsForValue().get("user:" + id);
    if (user != null) {
        return user;
    }
    
    // 2. 缓存未命中,查询数据库
    user = userMapper.selectById(id);
    
    // 3. 缓存穿透处理:如果数据库也不存在,缓存空值
    if (user == null) {
        // 缓存空值,过期时间设置较短(如 60 秒)
        redisTemplate.opsForValue().set("user:" + id, new User(), 60, TimeUnit.SECONDS);
        return null;
    }
    
    // 4. 数据库存在,写入缓存
    redisTemplate.opsForValue().set("user:" + id, user, 3600, TimeUnit.SECONDS);
    return user;
}
最佳实践: 空值缓存虽然能解决穿透问题,但会占用缓存空间。建议配合布隆过滤器使用:布隆过滤器判断 key 不存在时直接拒绝请求,避免无效缓存写入。布隆过滤器的误判率通常控制在 1% 以内。

1.6 缓存击穿的互斥锁方案

对于缓存击穿,最常用的方案是互斥锁(Mutex Key),确保同一时刻只有一个线程去加载数据:

// 缓存击穿:互斥锁方案
public Product getProductById(Long id) {
    String cacheKey = "product:" + id;
    String lockKey = "lock:" + id;
    
    // 1. 先查缓存
    Product product = redisTemplate.opsForValue().get(cacheKey);
    if (product != null) {
        return product;
    }
    
    // 2. 尝试获取分布式锁
    Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    if (Boolean.TRUE.equals(locked)) {
        try {
            // 3. 双重检查:获取锁后再次查询缓存
            product = redisTemplate.opsForValue().get(cacheKey);
            if (product != null) {
                return product;
            }
            // 4. 查询数据库
            product = productMapper.selectById(id);
            if (product != null) {
                redisTemplate.opsForValue().set(cacheKey, product, 3600, TimeUnit.SECONDS);
            }
            return product;
        } finally {
            // 5. 释放锁
            redisTemplate.delete(lockKey);
        }
    } else {
        // 6. 未获取到锁,等待后重试
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return getProductById(id); // 递归重试
    }
}

1.7 缓存雪崩的过期时间随机化

防止缓存雪崩的核心策略是避免大量 key 在同一时刻过期:

// 缓存雪崩:过期时间加随机值
public void setCacheWithRandomExpire(String key, Object value, long baseExpire) {
    // 基础过期时间上增加 0~300 秒的随机偏移
    long randomExpire = baseExpire + ThreadLocalRandom.current().nextLong(0, 300);
    redisTemplate.opsForValue().set(key, value, randomExpire, TimeUnit.SECONDS);
}

// 批量初始化缓存时使用
public void initProductCaches(List<Product> products) {
    for (Product product : products) {
        String key = "product:" + product.getId();
        // 每个 key 的过期时间都不同,避免集中失效
        setCacheWithRandomExpire(key, product, 3600);
    }
}
关键总结: 缓存穿透、击穿、雪崩是缓存系统中最常见的三类故障。穿透是"数据不存在"导致的持续穿透;击穿是"热点 key 过期"导致的瞬时穿透;雪崩是"大量 key 同时过期"导致的集体穿透。理解这三者的本质区别,是设计缓存一致性方案的第一步,也是后续章节中缓存更新策略、双写一致性、分布式锁等高级主题的基础。