SpringBoot 多级缓存(本地缓存 + Redis)

前面我们先后学习了 Redis 分布式缓存的整合、缓存三大核心问题(穿透、击穿、雪崩)的解决方案,以及缓存与数据库双写一致性的落地策略,这些内容已经能解决绝大多数单体、分布式项目的基础缓存需求。

但随着项目用户量、请求量不断上涨,尤其是面对秒杀、首页高频访问、商品详情页等高并发场景,你会发现:单纯只使用 Redis 缓存,依然存在明显的性能瓶颈,甚至会出现 Redis 扛不住流量、接口响应延迟飙升的情况。

举个真实生产场景:某电商首页,高峰期 QPS 达到 10 万+,所有请求都去访问 Redis 集群,导致 Redis 网络带宽打满、连接数超标,部分请求出现超时;即使 Redis 能扛住,每次请求的网络 IO 耗时(哪怕 1-2 毫秒),在 10 万+ QPS 下也会被无限放大,接口响应速度始终无法突破瓶颈。

为了解决这个问题,企业级高并发项目的终极缓存架构------多级缓存 就应运而生。它不是对 Redis 缓存的替代,而是在 Redis 之上,增加一层"本地内存缓存",形成 本地缓存(Caffeine) + Redis 分布式缓存 + 数据库 的三层缓存架构,层层拦截请求,极致压榨接口性能。

核心访问逻辑非常简单,记好这一句话就够了:请求优先走本地内存缓存 ,命中直接返回,无任何网络开销、速度最快;本地缓存未命中,再查 Redis 分布式缓存;Redis 依然未命中,最后查询数据库,查询到数据后,再依次回写到 Redis、本地缓存,供后续请求快速命中。

一、为什么要用多级缓存?单纯 Redis 不够用吗?

很多同学都会有这样的疑惑:Redis 已经是业界公认的高性能缓存中间件,毫秒级响应,为什么还要多此一举,再加一层本地缓存?其实答案很简单:极致性能的追求,以及对 Redis 集群的保护

我们先明确三种数据存储的读取速度对比,这是多级缓存设计的核心依据(记住这个排序,面试必问):

本地内存缓存(Caffeine) > Redis 网络缓存 > MySQL 数据库

我们用具体的耗时量级,更直观地感受差距:

  • • 本地缓存(Caffeine):数据存储在 JVM 堆内存中,无网络开销、无序列化/反序列化耗时 ,访问速度是 纳秒~微秒级(比如 100 纳秒,几乎可以忽略不计);

  • • Redis 缓存:独立的分布式中间件,请求需要经过"网络传输 + 序列化/反序列化 + Redis 内部查询",访问速度是 毫秒级(正常情况下 1-5 毫秒,网络波动时会更高);

  • • MySQL 数据库:数据存储在磁盘中,需要经过磁盘 IO、索引查询、事务处理,访问速度是 几十毫秒起步(复杂查询甚至会达到上百毫秒)。

举个例子:一个接口,单纯用 Redis 缓存,响应时间大概 5-10 毫秒;加上本地缓存后,热点请求命中本地,响应时间能降到 1 毫秒以内,性能提升 5-10 倍,这就是多级缓存的核心价值。

1. 单 Redis 缓存架构的核心痛点

单纯依赖 Redis 缓存,在高并发场景下,会暴露 3 个致命问题,也是我们必须引入多级缓存的原因:

(1)存在不可避免的网络开销

所有请求都要跨网络访问 Redis 集群(无论是本地部署还是远程部署),哪怕 Redis 性能极强,网络往返的耗时(哪怕 1 毫秒),在高并发场景下都会被无限放大。比如 10 万 QPS,每请求多 1 毫秒,总耗时就会增加 100 秒,直接导致接口响应延迟飙升。

(2)Redis 服务集群压力过大,易触发瓶颈

秒杀、首页轮播、商品详情等超级热点 Key,会被所有服务实例频繁请求。比如一个热点商品 Key,10 台服务实例,每台每秒请求 1000 次,总 QPS 就达到 1 万次,极易打满 Redis 的 QPS 上限(Redis 单机 QPS 大概 10 万左右,集群可扩展,但依然有上限)、CPU 使用率和网络带宽,导致 Redis 抖动、响应变慢。

(3)Redis 集群故障风险,易引发系统雪崩

Redis 虽然有集群、哨兵机制保证高可用,但依然可能出现抖动、断连、集群不可用的情况(比如网络故障、集群扩容失误)。一旦 Redis 不可用,所有请求会直接击穿到数据库,数据库无法承受高并发请求,会直接崩溃,引发整个系统雪崩。

2. 多级缓存架构的核心优势

引入本地缓存 + Redis 的多级缓存架构后,能完美解决上述痛点,同时带来 4 大核心优势,这也是企业高并发项目的标配:

  • 极致性能提升:热点数据全部常驻应用本地内存,微秒级响应,接口响应速度大幅提升,能轻松支撑 10 万+ QPS 场景;

  • 分流减压,保护 Redis:绝大部分高频请求会被本地缓存直接拦截,不再访问 Redis,极大降低 Redis 集群的 QPS、CPU、网络带宽压力,避免 Redis 成为系统瓶颈;

  • 高可用兜底,防止雪崩:即使 Redis 集群抖动、不可用,本地缓存依然可以扛住热点流量,避免请求直接击穿到数据库,为 Redis 恢复争取时间,提升系统整体可用性;

  • 架构分层合理,职责清晰:本地缓存负责扛"超级热点数据"(访问频率极高、数据量小),Redis 负责扛"全量分布式缓存"(数据量大、需要跨实例共享),数据库负责存储原始数据,分层各司其职,架构更稳定、可扩展。

面试必背总结:多级缓存的核心价值,是"用本地缓存的极致速度,分流 Redis 压力,同时为系统提供高可用兜底",解决单 Redis 缓存的网络开销、压力过大、故障雪崩三大痛点,实现性能与可用性的双重提升。

二、本地缓存选型对比(Caffeine、Guava、ConcurrentHashMap)

多级缓存的核心是"本地缓存",选择一个合适的本地缓存组件,直接决定了本地缓存的性能、命中率和稳定性。目前 Java 项目中,本地缓存主流有三种实现,我们做一个完整的对比(面试高频考点,必须吃透):

缓存组件 底层原理 性能表现 淘汰策略 内存占用 适用场景 SpringBoot 支持
ConcurrentHashMap JDK 自带线程安全 Map,基于数组+链表/红黑树实现 一般(仅满足基础缓存需求,无优化) 无自动淘汰策略,数据会无限堆积,需手动清理 高(无内存优化,数据存储冗余) 简单少量数据、小项目临时缓存、无需自动淘汰的场景 无内置支持,需手动封装
Guava Cache Google 开源本地缓存,基于 ConcurrentHashMap 封装 优秀(满足大部分中小型项目需求) LRU(最近最少使用)淘汰算法,支持过期时间 中等(有基础内存优化) 老项目遗留、旧系统维护、对性能要求不极致的场景 无内置支持,需手动引入依赖、封装
Caffeine 基于 Google Guava 优化,采用更高效的内存结构 极强(业界最优) ,比 Guava 快 10 倍以上 Window TinyLFU 算法(兼顾访问频率+访问时间,命中率最高) 低(内存优化极佳,占用小) 新项目、高并发场景、多级缓存本地层首选,企业级标准选型 SpringBoot 2.x 官方内置支持,无需复杂配置

企业级项目统一选型 Caffeine

无论是性能、淘汰算法、内存占用,还是 SpringBoot 的支持度,Caffeine 都是最优解,具体优势再强调 3 点,面试直接答:

  • • 性能最优:底层优化极致,访问速度比 Guava 快 10 倍以上,支持高并发场景下的快速访问;

  • • 命中率最高:采用 Window TinyLFU 淘汰算法,完美解决 LRU 算法"被冷数据冲掉热点数据"的问题,缓存命中率业界领先;

  • • 集成便捷:SpringBoot 2.x 官方内置支持,无需手动引入复杂依赖,配置简单,API 优雅,开发成本低。

注意:ConcurrentHashMap 仅适合临时缓存少量数据,绝对不能用于高并发多级缓存的本地层;Guava Cache 已逐渐被 Caffeine 替代,新项目不推荐使用。

三、多级缓存整体架构设计

多级缓存的核心是"分层拦截、依次回写",我们先明确 本地缓存(Caffeine)+ Redis + 数据库 三层缓存的完整访问链路,这是所有项目通用的标准流程,必须记牢:

1. 完整访问链路

    1. 用户请求通过网关进入微服务实例,首先访问 一级缓存:本地 Caffeine 缓存
    1. 如果本地缓存命中(数据存在且未过期),直接返回数据,整个流程结束,无任何网络 IO 开销,耗时微秒级;
    1. 如果本地缓存未命中(数据不存在或已过期),继续访问 二级缓存:Redis 分布式缓存
    1. 如果 Redis 缓存命中,返回数据,同时将数据 回写到本地 Caffeine 缓存(供后续请求快速命中),再返回给前端;
    1. 如果 Redis 缓存也未命中,最后访问 三级存储:MySQL 数据库
    1. 如果数据库命中数据,依次回写缓存:先写入 Redis(分布式共享),再写入本地 Caffeine 缓存(本地快速访问),最后返回数据给前端;
    1. 如果数据库也无数据,直接返回空(或默认值),不写入任何缓存(避免缓存穿透)。

2. 核心设计原则

多级缓存不是"本地缓存+Redis"的简单叠加,必须遵循以下 5 个设计原则,否则会出现内存溢出、数据不一致、性能不升反降等问题:

  • 本地缓存只存超级热点 Key:本地缓存是 JVM 堆内存,空间有限,绝对不能存放全量数据,只缓存访问频率极高、数据量小的超级热点 Key(比如首页轮播数据、秒杀商品信息),防止 JVM 内存溢出(OOM);

  • 每层缓存都必须设置过期时间和淘汰策略:本地缓存设置过期时间+最大容量+淘汰策略,Redis 设置过期时间,避免数据长期驻留、出现脏数据;

  • 两层缓存过期时间差异化设计 :核心原则------本地缓存 TTL < Redis 缓存 TTL,本地缓存先过期,流量回落 Redis,保证分布式场景下的数据统一;

  • 数据更新时,双缓存同步删除:更新/删除数据时,必须同时删除本地缓存和 Redis 缓存,坚决杜绝"只删一个缓存"的情况,保证双缓存一致性;

  • 必须解决分布式多实例下,本地缓存更新不同步问题:这是多级缓存最容易出问题的点,后面会专门拆解解决方案。

四、SpringBoot 整合多级缓存完整实现(Caffeine + Redis)

下面我们一步步实现 SpringBoot 与多级缓存的整合,从依赖引入、配置文件、缓存管理器、工具类,到业务层实战,代码全部可直接复制,注释详细,新手也能轻松落地。

前置环境:SpringBoot 2.7.x、Redis 6.x、MySQL 8.0,确保 Redis 已启动,数据库正常连接。

1. Maven 依赖

引入 SpringBoot 核心依赖,包含 Web、Spring Cache(统一缓存整合)、Redis、Caffeine、Lombok,无需额外引入其他依赖,SpringBoot 已内置 Caffeine 支持。

go 复制代码
<!-- SpringBoot Web 核心 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Cache 统一缓存整合(核心,用于整合本地缓存和Redis) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- Redis 分布式缓存依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Caffeine 本地缓存依赖(SpringBoot 2.x 内置,无需指定版本) -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

<!-- Lombok 简化开发(可选,建议引入) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!-- MyBatis-Plus 简化数据库操作(可选,也可使用原生MyBatis) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

<!-- MySQL 驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

2. application.yml 完整配置

统一配置 Redis、Caffeine 本地缓存、数据库,重点配置缓存过期时间、最大容量、序列化方式,避免出现缓存乱码、内存溢出等问题。

go 复制代码
spring:
  # 数据库配置(用于查询原始数据)
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: 123456

  # Redis 分布式缓存配置(二级缓存)
  redis:
    host: localhost
    port: 6379
    password: # 无密码留空,有密码填写实际密码
    database: 0 # 缓存专用数据库,避免与业务数据冲突
    lettuce:
      pool:
        maximum-pool-size: 20 # 最大连接数,根据高并发需求调整
        minimum-idle: 5 # 最小空闲连接,保证快速响应
        max-wait: 3000 # 最大等待时间,避免连接阻塞
    timeout: 5000 # 连接超时时间,单位:毫秒

  # Spring Cache 多级缓存统一配置(核心)
  cache:
    type: CAFFEINE # 默认使用 Caffeine 本地缓存,配合自定义缓存管理器实现多级缓存
    # Caffeine 本地缓存配置(一级缓存)
    caffeine:
      maximum-size: 10000 # 本地缓存最大条数(核心,防止OOM)
      expire-after-write: 60s # 本地缓存过期时间(60秒,比Redis短)
      initial-capacity: 1000 # 本地缓存初始容量,避免频繁扩容

# MyBatis-Plus 配置(可选,根据实际使用调整)
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.xxx.entity
  configuration:
    map-underscore-to-camel-case: true # 下划线转驼峰

# 日志配置(可选,用于调试缓存命中情况)
logging:
  level:
    com.xxx.service: debug
    org.springframework.cache: debug

✅ 关键配置说明:

  • caffeine.maximum-size: 10000:本地缓存最大条数,必须配置,防止 JVM 内存溢出,根据业务热点数据量调整(一般 1 万-10 万条);

  • caffeine.expire-after-write: 60s:本地缓存过期时间,60 秒,比 Redis 短(后面 Redis 配置 300 秒),实现差异化 TTL;

  • redis.database: 0:缓存专用数据库,与业务数据库分离,避免误操作删除业务数据;

  • cache.type: CAFFEINE:默认使用本地缓存,配合自定义缓存管理器,实现"本地+Redis"的多级缓存路由。

3. 多级缓存配置类(自定义缓存管理器)

SpringBoot 内置的缓存管理器,只能单独使用 Caffeine 或 Redis,无法实现"优先本地、再 Redis"的多级缓存逻辑。因此我们需要自定义缓存管理器,整合 Caffeine 和 Redis,实现自定义缓存路由。

同时配置 Redis 序列化方式(避免缓存乱码)、过期策略,确保多级缓存正常协同工作。

go 复制代码
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

/**
 * 多级缓存配置类
 * 整合 Caffeine 本地缓存 + Redis 分布式缓存,实现优先本地、再 Redis 的多级缓存逻辑
 */
@Configuration
@EnableCaching // 开启 Spring Cache 注解支持
public class MultiCacheConfig {

    /**
     * 1. 配置 Caffeine 本地缓存(一级缓存)
     * 这里单独配置 Caffeine 实例,可自定义不同缓存空间的过期时间、容量
     */
    @Bean
    public Caffeine<Object, Object> caffeineCache() {
        return Caffeine.newBuilder()
                .maximumSize(10000) // 最大容量
                .expireAfterWrite(Duration.ofSeconds(60)) // 过期时间 60秒
                .initialCapacity(1000) // 初始容量
                // 可选:配置缓存移除监听器,用于调试(生产可注释)
                .removalListener((key, value, cause) -> {
                    System.out.println("本地缓存移除:key=" + key + ", 原因=" + cause);
                });
    }

    /**
     * 2. 配置 Redis 缓存管理器(二级缓存)
     * 配置序列化方式、全局过期时间、自定义缓存空间
     */
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // Redis 缓存配置(全局)
        RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(300)) // Redis 全局过期时间 300秒(比本地长)
                // key 序列化:String 序列化,避免乱码
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                // value 序列化:JSON 序列化,支持复杂对象,避免乱码
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues(); // 不缓存 null 值,防止缓存穿透

        // 自定义不同缓存空间的过期时间(可选,比如商品缓存、用户缓存分开配置)
        // 比如:商品缓存过期时间 300秒,用户缓存过期时间 1800秒
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfig) // 应用全局配置
                .withCacheConfiguration("productCache", redisCacheConfig.entryTtl(Duration.ofSeconds(300)))
                .withCacheConfiguration("userCache", redisCacheConfig.entryTtl(Duration.ofSeconds(1800)))
                .build();
    }

    /**
     * 3. 自定义多级缓存管理器(核心)
     * 实现:优先查询本地 Caffeine 缓存,未命中再查询 Redis 缓存
     */
    @Bean
    public CacheManager multiLevelCacheManager(Caffeine<Object, Object> caffeine, RedisCacheManager redisCacheManager) {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        List<Cache> caches = new ArrayList<>();

        // 自定义缓存名称(与业务缓存空间对应,比如 productCache、userCache)
        String[] cacheNames = {"productCache", "userCache"};
        for (String cacheName : cacheNames) {
            // 每个缓存空间,都整合 Caffeine 本地缓存 + Redis 缓存
            CaffeineCache caffeineCache = new CaffeineCache(cacheName, caffeine.build());
            // 自定义缓存实现,重写 get 方法,实现优先本地、再 Redis 的逻辑
            Cache multiLevelCache = new Cache() {
                @Override
                public String getName() {
                    return cacheName;
                }

                @Override
                public Object getNativeCache() {
                    return caffeineCache.getNativeCache();
                }

                @Override
                public ValueWrapper get(Object key) {
                    // 1. 先查询本地 Caffeine 缓存
                    ValueWrapper localValue = caffeineCache.get(key);
                    if (localValue != null) {
                        return localValue;
                    }
                    // 2. 本地未命中,查询 Redis 缓存
                    Cache redisCache = redisCacheManager.getCache(cacheName);
                    ValueWrapper redisValue = redisCache.get(key);
                    if (redisValue != null) {
                        // 3. Redis 命中,回写到本地缓存
                        caffeineCache.put(key, redisValue.get());
                        return redisValue;
                    }
                    // 4. 都未命中,返回 null
                    return null;
                }

                @Override
                public <T> T get(Object key, Class<T> type) {
                    // 复用 get 方法逻辑,简化实现
                    ValueWrapper wrapper = get(key);
                    return wrapper != null ? type.cast(wrapper.get()) : null;
                }

                @Override
                public <T> T get(Object key, Callable<T> valueLoader) {
                    // 数据库查询逻辑,未命中时调用 valueLoader(查询数据库)
                    ValueWrapper wrapper = get(key);
                    if (wrapper != null) {
                        return (T) wrapper.get();
                    }
                    try {
                        T value = valueLoader.call();
                        if (value != null) {
                            // 数据库查询到数据,回写到本地和 Redis 缓存
                            put(key, value);
                        }
                        return value;
                    } catch (Exception e) {
                        throw new RuntimeException("多级缓存查询数据库失败", e);
                    }
                }

                @Override
                public void put(Object key, Object value) {
                    // 写入缓存:同时写入本地 Caffeine 和 Redis
                    caffeineCache.put(key, value);
                    Cache redisCache = redisCacheManager.getCache(cacheName);
                    redisCache.put(key, value);
                }

                @Override
                public void evict(Object key) {
                    // 删除缓存:同时删除本地 Caffeine 和 Redis
                    caffeineCache.evict(key);
                    Cache redisCache = redisCacheManager.getCache(cacheName);
                    redisCache.evict(key);
                }

                @Override
                public void clear() {
                    // 清空缓存:同时清空本地 Caffeine 和 Redis
                    caffeineCache.clear();
                    Cache redisCache = redisCacheManager.getCache(cacheName);
                    redisCache.clear();
                }
            };
            caches.add(multiLevelCache);
        }

        cacheManager.setCaches(caches);
        return cacheManager;
    }
}

✅ 核心说明:这个自定义缓存管理器,是多级缓存的核心,重写了 Cache 的 get、put、evict 等方法,实现了"优先本地、再 Redis、最后数据库"的完整逻辑,同时保证了双缓存的同步写入、同步删除,无需手动操作两层缓存。

4. 自定义多级缓存工具类

虽然自定义了缓存管理器,但为了进一步简化业务层代码,我们封装一个统一的多级缓存工具类,将缓存查询、删除、清空等操作封装起来,业务层直接注入调用,无需关注底层缓存逻辑,实现业务代码无侵入。

go 复制代码
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.function.Supplier;

/**
 * 多级缓存统一工具类
 * 封装缓存查询、写入、删除、清空操作,业务层直接调用,无需关注底层逻辑
 * 一级缓存:Caffeine 本地缓存
 * 二级缓存:Redis 分布式缓存
 * 三级存储:MySQL 数据库
 */
@Component
public class MultiLevelCacheUtil {

    @Resource
    private CacheManager multiLevelCacheManager;

    /**
     * 核心方法:多级缓存查询(自动回写)
     * 执行顺序:本地 Caffeine → Redis → 数据库(通过 supplier 提供)
     * @param cacheName 缓存空间名称(与配置类中一致,比如 productCache)
     * @param key 缓存 key(确保唯一,建议格式:业务模块:id,比如 product:1001)
     * @param dbSupplier 数据库查询函数式接口(未命中时调用,查询数据库)
     * @return 数据(可能为 null)
     */
    public <T> T get(String cacheName, String key, Supplier<T> dbSupplier) {
        // 获取对应的多级缓存
        Cache cache = multiLevelCacheManager.getCache(cacheName);
        if (cache == null) {
            throw new RuntimeException("多级缓存空间不存在:" + cacheName);
        }

        // 调用缓存管理器的 get 方法,自动执行 本地→Redis→数据库 流程
        return cache.get(key, dbSupplier);
    }

    /**
     * 写入多级缓存(同时写入本地 + Redis)
     * @param cacheName 缓存空间
     * @param key 缓存 key
     * @param value 缓存值
     */
    public void put(String cacheName, String key, Object value) {
        Cache cache = multiLevelCacheManager.getCache(cacheName);
        if (cache != null) {
            cache.put(key, value);
        }
    }

    /**
     * 删除多级缓存(同时删除本地 + Redis)
     * @param cacheName 缓存空间
     * @param key 缓存 key
     */
    public void delete(String cacheName, String key) {
        Cache cache = multiLevelCacheManager.getCache(cacheName);
        if (cache != null) {
            cache.evict(key);
        }
    }

    /**
     * 清空指定缓存空间的所有缓存(同时清空本地 + Redis)
     * @param cacheName 缓存空间
     */
    public void clearCache(String cacheName) {
        Cache cache = multiLevelCacheManager.getCache(cacheName);
        if (cache != null) {
            cache.clear();
        }
    }

    /**
     * 清空所有缓存空间的缓存(谨慎使用,一般用于系统重启、全量更新)
     */
    public void clearAllCache() {
        multiLevelCacheManager.getCacheNames().forEach(this::clearCache);
    }
}

5. 业务 Service 层实战使用

以商品服务为例,演示多级缓存的实际使用,业务层只需注入工具类,一行代码实现多级缓存查询、更新、删除,完全无侵入,代码简洁高效。

先定义实体类、Mapper 接口(简化示例,重点展示缓存使用):

go 复制代码
// 1. 商品实体类(Product.java)
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
public class Product {
    private Long id; // 商品ID
    private String name; // 商品名称
    private BigDecimal price; // 商品价格
    private Integer stock; // 库存
    private LocalDateTime createTime; // 创建时间
    private LocalDateTime updateTime; // 更新时间
}

// 2. 商品 Mapper 接口(ProductMapper.java)
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ProductMapper extends BaseMapper<Product> {
    // 继承 BaseMapper,无需手动编写查询、更新、删除方法
}

业务 Service 层实现:

go 复制代码
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;

/**
 * 商品服务,演示多级缓存实战使用
 */
@Service
public class ProductService {

    @Resource
    private ProductMapper productMapper;

    @Resource
    private MultiLevelCacheUtil multiLevelCacheUtil;

    // 缓存空间名称(与配置类中一致)
    private static final String CACHE_NAME = "productCache";
    // 缓存 key 前缀(避免 key 冲突,格式:业务模块:id)
    private static final String CACHE_KEY_PREFIX = "product:";

    /**
     * 多级缓存查询:根据商品ID查询商品
     * 自动执行:本地Caffeine → Redis → 数据库,未命中自动回写缓存
     */
    public Product getProductById(Long id) {
        // 构建缓存 key
        String cacheKey = CACHE_KEY_PREFIX + id;
        // 调用多级缓存工具类,dbSupplier 是数据库查询逻辑(函数式接口)
        return multiLevelCacheUtil.get(CACHE_NAME, cacheKey, () -> productMapper.selectById(id));
    }

    /**
     * 更新商品:更新数据库 + 同步删除双缓存(保证一致性)
     * 核心逻辑:先更数据库,再删缓存(避免双写顺序错误)
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateProduct(Product product) {
        // 1. 先更新数据库(事务保证原子性)
        productMapper.updateById(product);
        // 2. 同步删除本地缓存 + Redis 缓存(避免脏数据)
        String cacheKey = CACHE_KEY_PREFIX + product.getId();
        multiLevelCacheUtil.delete(CACHE_NAME, cacheKey);
    }

    /**
     * 删除商品:删除数据库 + 同步删除双缓存
     */
    @Transactional(rollbackFor = Exception.class)
    public void deleteProduct(Long id) {
        // 1. 先删除数据库
        productMapper.deleteById(id);
        // 2. 同步删除双缓存
        String cacheKey = CACHE_KEY_PREFIX + id;
        multiLevelCacheUtil.delete(CACHE_NAME, cacheKey);
    }

    /**
     * 手动刷新商品缓存(比如商品上下架、活动调价后,手动刷新)
     */
    public void refreshProductCache(Long id) {
        String cacheKey = CACHE_KEY_PREFIX + id;
        // 先删除旧缓存
        multiLevelCacheUtil.delete(CACHE_NAME, cacheKey);
        // 再查询数据库,重新写入缓存
        Product product = productMapper.selectById(id);
        if (product != null) {
            multiLevelCacheUtil.put(CACHE_NAME, cacheKey, product);
        }
    }
}

✅ 实战说明:

  • • 查询商品:只需调用工具类的 get 方法,传入缓存空间、key 和数据库查询逻辑,工具类会自动完成"本地→Redis→数据库"的查询和回写,业务层无需关注底层缓存逻辑;

  • • 更新/删除商品:先操作数据库,再调用工具类的 delete 方法,同步删除本地和 Redis 缓存,避免数据不一致;

  • • 手动刷新缓存:适合商品上下架、调价等场景,手动删除旧缓存、重新写入新数据,确保缓存数据最新。

五、多级缓存核心关键设计细节

多级缓存的落地,细节决定成败。以下 3 个核心设计细节,既是面试高频考点,也是生产环境必须遵循的原则,缺一不可。

1. 两层缓存过期时间设计(差异化 TTL)

前面反复强调:本地缓存 TTL< Redis 缓存 TTL,这是保证分布式场景下数据一致性的关键,我们再深入拆解原因,面试直接答:

  • • 防止 JVM 内存溢出:本地缓存是 JVM 堆内存,空间有限,短 TTL 能让数据快速过期,释放内存,避免数据无限堆积;

  • • 保证数据统一:本地缓存先过期,后续请求会去 Redis 查询最新数据,再回写到本地缓存,确保所有服务实例的本地缓存,都能同步到 Redis 的最新数据;

  • • 兜底保障:即使本地缓存出现脏数据,也会快速过期,从 Redis 拉取最新数据,避免脏数据长期驻留。

生产环境推荐配置(参考):

  • • 本地 Caffeine:TTL = 60-300 秒(根据热点数据更新频率调整);

  • • Redis:TTL = 300-1800 秒(是本地缓存的 5-10 倍);

  • • 特殊场景(比如秒杀商品):本地 TTL = 30 秒,Redis TTL = 180 秒,确保数据快速同步。

2. Caffeine 淘汰算法优势

Caffeine 的核心优势之一,就是其淘汰算法------Window TinyLFU,这也是它比 Guava Cache 性能好、命中率高的核心原因,我们拆解一下(不用深入底层,记住核心逻辑即可):

Guava Cache 使用的是 LRU(最近最少使用)算法,缺点很明显:容易被"一次性冷数据"冲掉热点数据。比如某条冷数据被大量请求一次(比如首页广告),会被 LRU 算法认为是"热点数据",从而挤掉真正长期访问的热点数据,导致缓存命中率下降。

而 Window TinyLFU 算法,兼顾了 访问频率(LFU)和访问时间(LRU),核心逻辑:

  • • 对数据的访问频率进行统计,优先保留访问频率高的数据;

  • • 引入"时间窗口"机制,对长期未访问的热点数据,逐步降低其频率权重,避免"僵尸热点数据"长期占用内存;

  • • 对一次性冷数据,直接过滤,不占用缓存空间,避免冲掉真正的热点数据。

面试总结:Caffeine 采用 Window TinyLFU 淘汰算法,兼顾访问频率和时间,缓存命中率高于 Guava Cache,能更好地适应高并发热点数据场景。

3. 本地缓存容量限制(防止 OOM)

这是新手最容易踩的坑:本地缓存不设置最大容量(maximumSize),导致数据无限堆积,最终引发 JVM 内存溢出(OOM),系统崩溃。

核心原则:本地缓存只存超级热点 Key,不存全量数据,同时必须配置 maximumSize,根据业务热点数据量调整,一般建议:

  • • 中小型项目:maximumSize = 1 万-5 万条;

  • • 大型高并发项目:maximumSize = 5 万-10 万条;

  • • 避免设置过大(比如 100 万条),否则会占用大量 JVM 内存,影响服务正常运行。

同时,可配合 Caffeine 的 removalListener(缓存移除监听器),监控本地缓存的移除情况,排查是否存在异常(比如大量热点数据被淘汰,可能是容量设置过小)。

如果你在实战中遇到问题,欢迎在评论区留言交流,一起避坑、一起进步!

别忘了点赞+在看+收藏三连,关注我,解锁更多 SpringBoot AOP 实战干货,下期再见❤️

相关推荐
2601_949816682 小时前
springcloud springboot nacos版本对应
spring boot·spring·spring cloud
C雨后彩虹2 小时前
文件目录大小
java·数据结构·算法·华为·面试
Deepincode2 小时前
Redis源码探究系列—字典(dict)结构源码详解(HashTable 实现)
redis
0南城逆流02 小时前
【技术点】嵌入式技术考点三:数据结构
java·数据结构·算法
随风,奔跑2 小时前
Spring Boot Alibaba(三)----Sentinel
spring boot·后端·sentinel
练习时长一年2 小时前
xlsx文件下载异常问题
java·开发语言
电魂泡哥2 小时前
Mysql索引下推、索引跳跃、索引覆盖
后端
渡边时雨2 小时前
大多数人搭 RAG,第一步就错了
后端·llm
ATCH IERV2 小时前
Redis6.2.6下载和安装
java