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