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 实战干货,下期再见❤️

相关推荐
吴阿福|一人公司几秒前
深度解析 Python 类变量修改的命名空间隔离
java·服务器·数据结构
zzz_23686 分钟前
【Java基础】链表的七十二变——从LRU缓存到手写浏览器前进后退
java·链表·缓存
番茄去哪了9 分钟前
神领物流面试题(一)
java·大数据·中间件
云烟成雨TD10 分钟前
Agent Scope Java 2.x 系列【9】接入高德 MCP 服务
java·人工智能·agent
布朗克16827 分钟前
39 Spring Boot Web实战
前端·spring boot·后端·实战
gaohe26AIliuzeyu28 分钟前
Java内部类
java·开发语言
西安邮电大学32 分钟前
有关数组的经典算法题
java·后端·其他·算法·面试
山东点狮信息科技有限公司32 分钟前
点狮HRM-HRM系统安全体系与数据保护方案
后端·安全·spring·spring cloud·微服务·系统安全·资产
互联网推荐官35 分钟前
上海AI Agent智能体开发公司技术选型实录:六条路径、三类架构与真实落地约束
java·人工智能·ai·架构·开发经验·上海
mikasa66742 分钟前
关于Spring MVC 基于 AOP 实现的全局控制器统一处理方案@ControllerAdvice
java·spring·mvc