SpringBoot+Caffeine+Redis实现多级缓存

多级缓存:架构设计、实战落地与问题解决

在高并发分布式系统中,缓存是提升接口响应速度、降低数据库压力的核心技术,但单一缓存层往往难以兼顾性能一致性高可用。多级缓存通过整合不同层级的缓存组件,扬长避短构建层次化缓存体系,成为支撑百万级 QPS 系统的标配方案。本文将从核心概念、架构设计、实战实现、一致性保障及经典问题解决五个维度,全面解析多级缓存技术,同时结合 SpringBoot+Caffeine+Redis 的实战案例,让技术落地更具可操作性。

一、什么是多级缓存?

多级缓存是指在系统中部署多层功能互补的缓存组件 ,让请求按照预设顺序依次访问各层缓存,仅当所有缓存层均未命中时,才访问底层数据库的架构模式。其核心设计思想是离用户越近的缓存,速度越快、开销越低,通过分层拦截请求,最大化减少对后端存储的访问。

在 Java 后端体系中,最主流的是二级缓存架构(本地缓存 + 分布式缓存),部分场景会结合数据库自身缓存形成三级体系,各层核心特性与选型如下:

  1. 本地缓存 :运行在应用进程内部的内存缓存,无网络 IO 开销,读写延迟纳秒级。主流选型为 Caffeine(性能优于 Guava Cache、Ehcache),适合存储高频访问的热点数据,缺点是数据仅当前实例可见,无法分布式共享,且受 JVM 内存限制。
  2. 分布式缓存 :独立于应用的中间件,部署在集群中可被所有应用实例共享,数据一致性更易保障。主流选型为 Redis(支持丰富数据结构、持久化、分布式锁),适合存储全量热点数据 + 通用业务数据,缺点是存在网络 IO 开销,性能略低于本地缓存。
  3. 数据库缓存:数据库自身的查询缓存 / 缓冲池(如 MySQL 的 InnoDB Buffer Pool),属于底层被动缓存,通常无需人工干预,仅作为最后一道数据屏障。

各层缓存性能对比(核心指标):

缓存层级 读写延迟 集群共享 部署成本 适用场景
本地缓存(Caffeine) 纳秒级 低(无独立组件) 高频热点数据、单机维度临时数据
分布式缓存(Redis) 微秒级 中(需集群部署) 全量热点数据、跨实例共享数据
数据库缓存 毫秒级 所有数据的最终存储

二、为什么需要多级缓存?

单一缓存层在高并发场景下存在难以克服的短板,而多级缓存通过优势互补解决这些问题,同时带来极致的性能提升。我们通过一组电商商品详情页的压测数据,直观感受多级缓存的价值:

架构模式 QPS TP99 延迟 (ms) 数据库负载 缓存命中率
单 Redis 缓存 5.8 万 120 65% 75%
Caffeine+Redis 多级缓存 120 万 45 8% 99.2%

从数据可以看出,多级缓存让 QPS 提升 20 倍、TP99 延迟降低 60%、数据库负载减少 87%,核心价值体现在三个方面:

  1. 极致性能:本地缓存拦截 80% 以上的高频请求,避免大量请求穿透到 Redis,减少网络 IO 和 Redis 集群压力,让接口响应速度达到极致。
  2. 高可用兜底:当分布式缓存集群(如 Redis)发生宕机时,本地缓存可临时兜底核心热点数据,避免系统直接雪崩,提升服务容错能力。
  3. 资源优化:通过分层存储数据,将高频数据放在本地缓存(低开销),通用数据放在分布式缓存(共享性),避免 Redis 存储大量低价值数据,降低缓存集群的部署成本。

此外,多级缓存还能从架构层面规避单一缓存的经典问题,比如本地缓存的分布式一致性问题可通过 Redis 兜底,Redis 的网络开销问题可通过本地缓存拦截,让系统更健壮。

三、多级缓存核心架构设计

3.1 核心设计原则

多级缓存的设计需遵循3 个核心原则,否则易导致架构臃肿、数据一致性混乱:

  1. 请求就近原则:请求优先访问本地缓存,未命中再访问分布式缓存,最后访问数据库,反向流程不可行。
  2. 数据分层原则 :不同价值的数据存储在对应层级,本地缓存仅存高频热点数据 (控制容量,避免 OOM),分布式缓存存全量业务数据,不重复存储低价值数据。
  3. 失效由上至下原则:缓存更新 / 失效时,先操作本地缓存,再操作分布式缓存,避免出现 "本地缓存有旧数据,分布式缓存有新数据" 的不一致情况。

3.2 主流架构:Caffeine+Redis 二级缓存

Java 后端中,Caffeine(本地)+ Redis(分布式) 是工业级落地的主流架构,适用于 90% 以上的业务场景(电商、社交、资讯等),其请求访问流程数据更新流程如下,是后续实战的核心基础。

(1)请求访问流程(读操作)
  1. 应用接收到请求后,首先查询Caffeine 本地缓存,命中则直接返回结果,结束请求;
  2. 本地缓存未命中,查询Redis 分布式缓存,命中则将数据回写到本地缓存(方便后续请求拦截),然后返回结果;
  3. 分布式缓存也未命中,执行数据库查询,查询结果非空时,依次回写到 Redis 和 Caffeine,然后返回结果;
  4. 数据库查询结果为空时,执行空值缓存(短期过期),避免后续请求穿透到数据库。
(2)数据更新流程(写操作)

遵循 "先更数据库,再删缓存" 原则(避免 "先删缓存,再更数据库" 的并发不一致问题),核心步骤:

  1. 执行数据库的增 / 删 / 改操作,保证数据落地;
  2. 删除 Redis 对应缓存(而非更新,避免并发覆盖);
  3. 驱逐 Caffeine 本地缓存(当前实例),若为集群部署,通过消息队列(如 RocketMQ/Kafka)通知其他实例驱逐本地缓存;
  4. 后续请求会触发缓存重新加载,保证数据最新。

注意 :写操作优先选择删缓存 而非更缓存 ,原因是:若同时有多个写请求,直接更新缓存可能导致并发覆盖,而删除缓存后,由读请求触发懒加载,能保证缓存数据的最终一致性。

四、实战落地:SpringBoot 整合 Caffeine+Redis 多级缓存

本节基于SpringBoot 2.7.x ,实现 Caffeine+Redis 的二级缓存架构,包含核心依赖、配置、代码实现,同时整合空值缓存分布式锁等特性,直接可用于生产环境(仅需根据业务调整参数)。

4.1 核心依赖引入(Maven)

引入 Caffeine、Redis、SpringCache 核心依赖,简化缓存操作,无需手动封装缓存工具类:

xml 复制代码
<!-- SpringCache缓存抽象(简化缓存注解使用) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine本地缓存 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>
<!-- Redis分布式缓存(Lettuce客户端) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 序列化依赖(解决Redis存储对象乱码问题) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.32</version>
</dependency>

4.2 核心配置(配置类 + yml)

(1)缓存配置类:初始化 Caffeine 和 Redis 缓存管理器

通过配置类实现双缓存管理器,分别管理本地缓存和分布式缓存,支持注解式缓存操作,核心参数按需调整(如过期时间、最大容量):

java 复制代码
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching // 开启SpringCache注解支持
public class MultiLevelCacheConfig {

    // 1. 本地缓存管理器(Caffeine):优先使用,存储热点数据
    @Bean("caffeineCacheManager")
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        // 核心配置:最大容量1000条、写入后5秒过期(短过期防脏数据)、弱引用回收
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(5, TimeUnit.SECONDS)
                .weakValues());
        return cacheManager;
    }

    // 2. 分布式缓存管理器(Redis):@Primary表示默认缓存管理器,存储通用数据
    @Bean("redisCacheManager")
    @Primary
    public CacheManager redisCacheManager(RedisConnectionFactory factory) {
        // 序列化配置:解决对象乱码
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .entryTtl(30, TimeUnit.SECONDS); // 写入后30秒过期

        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }
}
(2)yml 配置:Redis 连接信息
yaml 复制代码
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    lettuce:
      pool:
        max-active: 8 # 最大连接数
        max-idle: 8   # 最大空闲连接
        min-idle: 2   # 最小空闲连接
        max-wait: 1000ms # 连接等待时间
    timeout: 3000ms # 连接超时时间

4.3 业务层实现:注解式多级缓存操作

基于 SpringCache 注解,实现商品详情查询商品库存更新的核心业务,完美贴合前文的 "请求访问流程" 和 "数据更新流程",代码简洁易维护:

java 复制代码
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;

@Service
public class ProductService {

    @Resource
    private ProductMapper productMapper; // 数据库Mapper
    @Resource
    private StringRedisTemplate redisTemplate;

    // 空值标记:解决缓存穿透
    private static final Product NULL_PRODUCT = new Product(-1L, "NULL", 0, 0.0);

    /**
     * 商品详情查询:多级缓存(先Caffeine,再Redis,最后DB)
     * @Cacheable:缓存查询结果,指定本地缓存管理器
     */
    @Cacheable(value = "product", key = "#id", cacheManager = "caffeineCacheManager")
    public Product getProduct(Long id) {
        // 1. 本地缓存未命中,查询Redis
        String redisKey = "product:" + id;
        Product redisProduct = redisTemplate.opsForValue().get(redisKey);
        if (redisProduct != null) {
            return redisProduct;
        }

        // 2. Redis未命中,查询数据库
        Product dbProduct = productMapper.selectById(id);
        // 3. 空值缓存:防止穿透
        Product cacheProduct = Optional.ofNullable(dbProduct).orElse(NULL_PRODUCT);
        // 4. 回写Redis:设置60秒过期
        redisTemplate.opsForValue().set(redisKey, cacheProduct, 60, TimeUnit.SECONDS);
        // 5. 若为NULL_PRODUCT,返回null,避免业务层处理异常
        return cacheProduct.equals(NULL_PRODUCT) ? null : cacheProduct;
    }

    /**
     * 商品库存更新:先更DB,再删缓存(保证一致性)
     * @CacheEvict:删除指定缓存
     */
    @CacheEvict(value = "product", key = "#productId", cacheManager = "caffeineCacheManager")
    public void updateProductStock(Long productId, int delta) {
        // 1. 更新数据库:先落地数据
        productMapper.updateStock(productId, delta);
        // 2. 删除Redis缓存:避免旧数据
        String redisKey = "product:" + productId;
        redisTemplate.delete(redisKey);
        // 3. 本地缓存已通过@CacheEvict删除,无需手动操作
    }
}

4.4 集群化改造:本地缓存一致性

上述代码仅适用于单机部署,集群部署时 ,某一实例更新数据后,其他实例的本地缓存仍会存在旧数据,导致数据不一致。解决该问题的主流方案是基于消息队列的缓存通知,核心思路:

  1. 部署消息队列(如 RocketMQ/Kafka),创建缓存更新主题
  2. 当某一实例执行缓存删除操作时,向主题发送缓存失效消息(包含缓存 key、缓存名称);
  3. 所有应用实例均作为消费者,监听该主题,收到消息后,删除本地对应缓存;
  4. 保证消息的至少一次消费,避免缓存失效消息丢失。

简化实现:使用 Redis 的 Pub/Sub 替代消息队列(轻量、无需独立部署),实现缓存通知,适合中小型集群。

五、多级缓存的一致性保障

多级缓存的核心痛点是数据一致性 ------ 各层缓存之间、各实例的本地缓存之间,可能因更新延迟、缓存污染导致数据不一致。由于强一致性会引入分布式事务、锁等机制,导致性能大幅下降,工业级场景中均采用最终一致性(允许短时间不一致,最终所有缓存层数据收敛),核心保障策略分为三类:

5.1 缓存更新策略:选对策略,从源头减少不一致

主流的缓存更新策略有 3 种,结合多级缓存的特性, "先更 DB,再删缓存" 是最优选择,其他策略仅适用于特殊场景:

  1. 先更 DB,再删缓存 (推荐):无并发覆盖问题,实现简单,仅存在 "删缓存失败" 的小概率问题,可通过定时任务补偿解决;
  2. 先删缓存,再更 DB:高并发下会出现 "缓存脏数据"(A 删缓存→B 查缓存未命中→查旧 DB→回写缓存→A 更 DB),导致缓存数据长期不一致;
  3. 双写策略(更新 DB 同时更新缓存):高并发下会出现 "缓存覆盖"(A 和 B 同时更新 DB,再先后更新缓存,导致缓存数据与最新 DB 数据不一致),仅适用于低并发场景。

5.2 过期时间策略:强制让脏数据失效

为各层缓存设置合理的过期时间(TTL) ,是最终一致性的最后一道屏障,即使出现缓存更新失败,脏数据也会在过期后自动失效,重新从 DB 加载最新数据。

  • 本地缓存(Caffeine):设置短 TTL(5-10 秒),快速淘汰脏数据,避免本地缓存长期不一致;
  • 分布式缓存(Redis):设置中等 TTL(30-60 秒),兼顾缓存命中率和数据新鲜度;
  • 空值缓存:设置极短 TTL(3-5 分钟),避免无效空值占用过多缓存空间。

同时,为 Redis 缓存设置随机 TTL 偏移量(如 30 秒 ±5 秒),避免大量缓存同时过期导致的雪崩问题。

5.3 异步同步策略:解决集群本地缓存一致性

如前文所述,集群部署时的本地缓存一致性,通过 "消息通知 + 异步删除" 实现,主流方案对比:

同步方案 实现难度 性能 适用场景
Redis Pub/Sub 中小型集群、对一致性要求一般的场景
消息队列(RocketMQ/Kafka) 大型集群、核心业务场景
分布式配置中心(Nacos/Apollo) 配置类缓存、低频更新数据

六、多级缓存经典问题解决

缓存系统的三大经典问题 ------穿透、击穿、雪崩 ,在多级缓存架构中可通过分层防护实现更高效的解决,相比单一缓存层,防护手段更丰富、效果更彻底。

6.1 缓存穿透:拦截无效请求,避免穿透到 DB

问题定义 :请求查询的数在所有缓存层和 DB 中均不存在,导致每次请求都穿透到 DB,若遭遇恶意请求(如伪造不存在的商品 ID),会压垮数据库。多级缓存防护方案(双重防护,层层拦截):

  1. 第一层:接口层参数校验:在 Controller/Gateway 层对请求参数做强校验(如 ID 必须大于 0、符合业务规则),直接拦截明显无效的请求;
  2. 第二层:空值缓存:在 Caffeine 和 Redis 中缓存空值(如前文的 NULL_PRODUCT),设置短 TTL,让后续相同无效请求被缓存拦截;
  3. 进阶方案:布隆过滤器 :将 DB 中所有有效主键(如商品 ID、用户 ID)预先加载到布隆过滤器,请求先经过过滤器校验,不存在的键直接拦截,仅可能存在的键才进入缓存层,适合超大规模数据场景(如千万级商品库)。

6.2 缓存击穿:保护热点数据,避免单点穿透

问题定义 :某个超高访问量的热点数据 (如秒杀商品)在缓存中过期的瞬间,大量并发请求同时穿透到 DB,导致数据库单点压力骤增。多级缓存防护方案(热点数据专属防护):

  1. 第一层:热点数据永不过期 :对秒杀、首页焦点图等核心热点数据,设置物理永不过期 ,通过手动更新 / 删除控制缓存生命周期,从源头避免过期;
  2. 第二层:本地锁 + 分布式锁双重锁 :缓存过期后,通过本地锁(synchronized) 拦截单机内的并发请求,再通过Redis 分布式锁(Redisson) 拦截集群内的并发请求,仅允许一个线程查询 DB 并更新缓存,其他线程等待缓存更新后再查询;
  3. 第三层:热点数据预热:系统启动时 / 流量高峰前,通过定时任务将热点数据主动加载到 Caffeine 和 Redis 中,避免缓存冷启动导致的击穿。

6.3 缓存雪崩:分散过期时间,避免集体穿透

问题定义 :大量缓存数据在同一时间点过期 ,或分布式缓存集群(Redis)宕机,导致所有请求瞬间涌向 DB,引发数据库雪崩,进而导致整个系统瘫痪。多级缓存防护方案(架构级防护,容错性拉满):

  1. 第一层:差异化 TTL :为 Redis 缓存设置随机过期时间偏移量(如基础 TTL30 秒 + 随机 0-5 秒),让缓存过期时间分散,避免集体失效;
  2. 第二层:多级缓存兜底:即使 Redis 集群宕机,Caffeine 本地缓存仍能拦截大部分热点请求,保证核心业务可用,同时触发告警机制;
  3. 第三层:缓存高可用 + 服务降级:Redis 采用 Cluster/Sentinel 集群部署,避免单点故障;同时结合 Hystrix/Sentinel 实现服务降级,当 DB 压力达到阈值时,直接返回本地缓存的兜底数据,放弃部分数据新鲜度,保证服务可用性;
  4. 第四层:数据库限流:在 DB 代理层(如 MyCat)设置访问限流,控制每秒访问 DB 的请求数,避免数据库被压垮。

七、多级缓存调优与监控

7.1 性能调优关键参数

多级缓存的性能调优核心是平衡缓存命中率和资源占用,关键参数调整原则:

  1. Caffeine 调优maximumSize根据 JVM 内存合理设置(如单机 1G 内存设置 1000-2000 条),避免 OOM;优先使用expireAfterWrite(写入后过期)而非expireAfterAccess(访问后过期),减少性能开销;
  2. Redis 调优 :开启持久化(RDB+AOF 混合),避免数据丢失;优化连接池参数(如max-active根据并发量设置),减少连接等待;使用 Redis Cluster 集群,提升并发能力;
  3. JVM 调优 :开启 G1GC,设置-XX:MaxGCPauseMillis=100,减少 GC 停顿对本地缓存的影响;合理设置 JVM 堆内存,为本地缓存预留足够空间。

7.2 核心监控指标

无监控不架构,多级缓存需要监控各层缓存的核心指标,及时发现缓存失效、命中率低等问题,主流监控方案为 Prometheus+Grafana:

  1. 缓存命中率:核心指标,本地缓存(Caffeine)命中率应≥90%,Redis 命中率应≥95%,命中率过低需分析数据分布,调整缓存策略;
  2. 缓存未命中率:持续偏高需排查是否存在穿透、击穿问题;
  3. Redis 指标:CPU 使用率、内存使用率、网络 IO、连接数,避免 Redis 成为性能瓶颈;
  4. 数据库指标:QPS、连接数、慢查询数,验证缓存拦截效果,若数据库 QPS 持续偏高,需优化缓存策略。

监控实现 :Caffeine 自带监控指标,可通过Cache.stats()获取;Redis 可通过 Exporter 采集指标;最终在 Grafana 中制作可视化大盘,设置指标告警(如命中率低于 90% 时触发短信 / 钉钉告警)。

八、大厂落地最佳实践

多级缓存在阿里、京东、拼多多等大厂的高并发场景中已广泛落地,总结其核心落地经验,让技术落地更贴合生产环境:

  1. 数据分层存储 :本地缓存仅存TOP 10% 的高频热点数据,其余数据存在 Redis,避免本地缓存占用过多 JVM 内存;
  2. 避免过度设计:中小型系统优先使用 "Caffeine+Redis" 二级架构,无需引入更多缓存层,增加架构复杂度;
  3. 缓存更新失败补偿 :针对 "删缓存失败" 问题,增加定时任务补偿(如每分钟扫描 DB 更新日志,对比缓存数据,删除不一致的缓存);
  4. 冷启动防护:系统重启时,先通过预热脚本加载热点数据,再对外提供服务,避免冷启动导致的缓存穿透、击穿;
  5. 灰度发布:缓存策略变更(如 TTL 调整、数据分层变更)时,采用灰度发布,逐步扩大范围,避免全量发布导致的性能问题。

九、总结

多级缓存并非简单的 "本地缓存 + 分布式缓存" 组合,而是一套兼顾性能、一致性、高可用的层次化架构设计思想。其核心价值在于通过分层拦截请求,将性能做到极致,同时通过最终一致性策略、分层防护手段,解决缓存的经典问题,成为支撑百万级 QPS 高并发系统的核心技术。

本文结合 SpringBoot+Caffeine+Redis 的实战案例,从架构设计到代码实现,从一致性保障到问题解决,实现了技术的全链路落地。在实际项目中,无需生搬硬套,可根据业务规模、并发量、数据一致性要求,灵活调整缓存架构和策略 ------ 中小型系统优先保证落地简单,大型高并发系统重点做好缓存分层、监控和容错。

缓存的本质是用空间换时间,而多级缓存则是让这份 "交换" 的性价比达到最高,这也是其能成为高并发系统标配的根本原因。

相关推荐
Java陈序员16 小时前
企业级!一个基于 Java 开发的开源 AI 应用开发平台!
spring boot·agent·mcp
杨运交1 天前
[041][公共模块]分布式唯一ID生成器设计与实现:一款灵活可扩展的雪花算法框架
spring boot
用户3074596982072 天前
Redis 延时队列详解
redis
烤代码的吐司君2 天前
Redis 数据结构 ZSet, BIT, HyperLogLog,Geo 空间数据
redis·后端
Flittly2 天前
【AgentScope Java新手村系列】(14)人机交互
java·spring boot·spring
RainCity2 天前
Java Swing 自定义组件库分享(十二)
java·笔记·后端
Flynt3 天前
从Spring Boot 4.0升到4.1,我在Maven和gRPC上栽了跟头
java·spring boot·后端
掉鱼的猫4 天前
Spring Boot → Solon 注解迁移实战指南:一张对照表说清楚
java·spring boot
leeyi4 天前
Checkpoint 机制:Agent 怎么在断电后接着跑
redis·aigc·agent
人活一口气5 天前
Spring Boot与AIGC的完美结合:从零搭建智能内容生成平台
java·spring boot·aigc