Spring 缓存体系全景概览
在现代 Java 企业级应用开发中,Spring 三级缓存是系统性能优化与高并发场景下的关键组件。Spring 框架通过 Cache 抽象层,实现了统一的缓存操作接口,并支持多种缓存实现(如 ConcurrentHashMap、Ehcache、Redis、Caffeine 等)。其中,三级缓存机制并非指 Spring 自身的三类缓存实现,而是指在 Spring 框架中,缓存分层设计与调用链路所形成的逻辑三层结构:
- 一级缓存:本地缓存(Local Cache),通常基于
ThreadLocal或ConcurrentHashMap实现,作用域为单次请求或单个线程,生命周期短; - 二级缓存:分布式缓存(Distributed Cache),如 Redis、Ehcache 集群,用于跨节点共享数据,提升系统横向扩展能力;
- 查询缓存(Query Cache):MyBatis / Hibernate 等 ORM 框架中的二级缓存扩展,结合 SQL 语句缓存结果集,适用于高频读、低频写场景。
在 Spring 中,@Cacheable、@CacheEvict、@CachePut 等注解底层依赖于 CacheManager 与 Cache 接口,而 Spring Boot 默认使用 SimpleCacheManager(基于 ConcurrentHashMap)。实际项目中,常通过整合 Redis 构建高可用的二级缓存架构,实现缓存穿透、击穿、雪崩的综合防护。
⚡ 为什么 Spring 缓存被称为“三级”?
“三级缓存”是开发者社区对 Spring 缓存分层调用链路的形象概括,而非 Spring 官方术语。其本质是:
- 调用链第一层:Spring AOP 拦截器(
CacheInterceptor)触发缓存逻辑; - 第二层:
CacheManager获取具体Cache实例(如RedisCache); - 第三层:底层缓存实现(如 Redis、Caffeine)执行存储/读取操作。
因此,“三级”强调的是缓存操作的分层调用与职责分离,而非 Spring 内部定义的三个固定缓存类型。
Spring 缓存核心机制深度解析
Spring 缓存基于 AOP 代理 + 策略模式 实现,其核心组件包括:
⚙️ 核心组件关系图
| 组件 | 职责 | 实现类示例 |
|---|---|---|
Cache |
缓存接口,定义 put/get/evict 等操作 | RedisCache、ConcurrentMapCache |
CacheManager |
管理多个 Cache 实例 |
RedisCacheManager、SimpleCacheManager |
CacheResolver |
动态解析缓存名称(如基于 SpEL 表达式) | DefaultCacheResolver |
KeyGenerator |
生成缓存键(Key) | SimpleKeyGenerator、DefaultKeyGenerator |
1. 缓存注解机制流程
以 @Cacheable 为例,其执行流程如下:
- 方法调用前,
CacheInterceptor拦截并解析缓存名称(cacheNames); - 通过
CacheResolver获取对应Cache实例; - 调用
KeyGenerator生成缓存 Key; - 尝试从
Cache中获取值: - 命中缓存 → 直接返回缓存值,跳过原方法;
- 未命中 → 执行原方法,将结果存入缓存后返回。
该流程可表示为伪代码:
if (cache.containsKey(key)) {
return cache.get(key);
}
Object result = method.invoke();
cache.put(key, result);
return result;
2. 缓存键(Key)生成策略详解
默认 SimpleKeyGenerator 使用以下规则生成 Key:
- 无参数 →
SimpleKey.EMPTY - 单个参数 → 参数值本身(如
String、Long) - 多个参数 →
new SimpleKey(params)
但该策略在参数为对象时存在风险(未实现 hashCode 和 equals 会导致 Key 冲突)。推荐自定义 KeyGenerator:
〔示例〕基于参数字段的 Key 生成器
@Bean
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(method.getName());
for (Object param : params) {
if (param == null) {
sb.append(":null");
} else if (param instanceof String || param instanceof Number) {
sb.append(":").append(param);
} else {
// 假设实体类有 id 字段
try {
Method getId = param.getClass().getMethod("getId");
sb.append(":").append(getId.invoke(param));
} catch (Exception e) {
sb.append(":").append(System.identityHashCode(param));
}
}
}
return sb.toString();
};
}
3. 缓存穿透、击穿、雪崩防护策略
缓存穿透:查询不存在数据
现象:大量请求查询数据库中不存在的数据(如 ID 为负数),缓存未命中,请求直达数据库,造成压力。
解决方案:
- 布隆过滤器:在缓存前增加布隆过滤器,快速过滤无效 Key;
- 缓存空值:对查询结果为空的数据也缓存(设置较短 TTL,如 60s);
- 参数校验:前置校验 ID 合法性(如 >0)。
缓存击穿:热点 Key 过期
现象:某个热点数据缓存过期瞬间,大量请求并发访问数据库。
解决方案:
- 互斥锁:仅允许一个线程重建缓存,其他线程等待;
- 逻辑过期:缓存中存储过期时间戳,异步刷新;
- 永不过期 + 定时更新:结合定时任务主动刷新。
缓存雪崩:大量 Key 同时过期
现象:大量缓存 Key 在同一时间过期,导致数据库瞬间压力激增。
解决方案:
- 随机过期时间:TTL 设置为 base + random(0, 300s);
- 多级缓存:本地缓存(Caffeine) + 分布式缓存(Redis);
- 限流降级:使用 Sentinel 或 Hystrix 保护数据库。
Spring 三级缓存详解:从本地到分布式
尽管 Spring 未明确定义“三级缓存”,但在实际项目中,开发者常按以下三层结构设计缓存体系:
⚡ 一级缓存:本地缓存(Local Cache)
特点:
- 生命周期短(单请求/线程级);
- 零网络开销,读写性能极高;
- 数据不共享,无法跨节点同步。
典型实现:
- ThreadLocal + Map:用于单线程内数据复用(如 MyBatis 的 Executor 一级缓存);
- Caffeine:高性能本地缓存库,支持 LRU、TTL、W-TinyLFU 策略。
示例:Spring 中使用 Caffeine 作为一级缓存
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("userCache");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats());
return cacheManager;
}
⚙️ 二级缓存:分布式缓存(Distributed Cache)
特点:
- 跨节点共享数据;
- 支持高并发读写;
- 需解决一致性、网络延迟、故障转移等问题。
主流方案:
| 缓存系统 | 优势 | 适用场景 |
|---|---|---|
| Redis | 高性能、多数据结构支持、集群高可用 | 通用场景、分布式会话、计数器 |
| Ehcache 3.x | 轻量级、支持堆外内存、持久化 | 单机缓存、嵌入式应用 |
| Memcached | 简单协议、分布式设计 | 高吞吐读场景(如图片缓存) |
Spring Boot 集成 Redis 示例:
spring:
cache:
type: redis
redis:
host: 127.0.0.1
port: 6379
timeout: 2000
lettuce:
pool:
max-active: 100
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
}
〔补充〕查询缓存(Query Cache)
查询缓存是 ORM 框架(如 MyBatis、Hibernate)的扩展功能,用于缓存 SQL 查询结果集,常与二级缓存配合使用:
- MyBatis:通过
<cache>标签开启二级缓存,结合useCache="true"启用查询缓存; - Hibernate:通过
hibernate.cache.use_query_cache=true开启查询缓存,配合CacheMode控制。
注意:查询缓存需结合 二级缓存(实体缓存)使用,否则仅缓存 ID 列表,仍需二次查询实体。
三级缓存协作流程图
① 请求到达 Spring Service 层
方法被 @Cacheable 注解修饰,触发缓存拦截逻辑。
② 本地缓存(Caffeine)查询
若命中本地缓存,直接返回;否则进入分布式缓存查询。
③ 分布式缓存(Redis)查询
若命中 Redis 缓存,更新本地缓存并返回;否则查询数据库。
④ 数据库查询 + 缓存回填
查询数据库后,将结果写入 Redis,并异步更新本地缓存。
Spring 缓存高频面试题深度解析
以下整理了 2024 年互联网大厂 Spring 缓存相关面试真题,覆盖原理、源码、实战问题,每题均附详细解析。
Q1:Spring 缓存注解的工作原理?
参考答案:
- Spring 通过 AOP 代理拦截被
@Cacheable等注解修饰的方法; - 核心类为
CacheInterceptor,实现MethodInterceptor接口; - 执行流程:
方法调用 → CacheInterceptor → CacheResolver → Cache → KeyGenerator → 方法体; - 若缓存命中,直接返回;否则执行方法后写入缓存。
扩展点:
- 可自定义
CacheResolver实现动态缓存名; - 可通过
CacheErrorHandler自定义异常处理逻辑。
Q2:@Cacheable 的 key 生成规则是什么?
参考答案:
- 默认使用
SimpleKeyGenerator: - 无参数 →
SimpleKey.EMPTY - 单个参数 → 参数值(需重写 hashCode/equals)
- 多个参数 →
new SimpleKey(params) - 推荐自定义 KeyGenerator,避免对象未重写 hashCode 导致 Key 冲突;
- 也可直接在注解中指定
key = "#id"或 SpEL 表达式(如#user.id)。
Q3:缓存与数据库双写一致性如何保证?
参考答案:
| 策略 | 适用场景 | 问题 |
|---|---|---|
| 先更新 DB → 再删缓存 | 读多写少 | 删缓存失败导致脏数据 |
| 先删缓存 → 再更新 DB | 写操作频繁 | 并发时可能读到旧数据 |
| 延时双删 | 高一致性要求 | 需异步任务保障 |
| 基于 Binlog 异步同步 | 大型系统 | 架构复杂,需中间件支持 |
推荐方案:结合 分布式锁 + 重试机制 确保缓存重建可靠性。
Q4:如何解决缓存穿透问题?
参考答案:
- 布隆过滤器:在缓存前拦截无效请求(空间效率高,但有误判率);
- 缓存空值:对查询结果为空的数据缓存(TTL 设为 60s,避免长期占用内存);
- 参数校验:前置校验 ID 范围(如 >0)、类型合法性。
Q5:Spring 缓存与 Redis 缓存的区别?
参考答案:
- Spring Cache 是抽象层,提供统一 API,底层可对接 Redis、Ehcache 等;
- Redis 是具体缓存实现,支持分布式、持久化、集群;
- Spring Cache 注解(如
@Cacheable)最终通过RedisCacheManager调用 Redis; - 二者是“接口与实现”关系,非替代关系。
常见问题(FAQ)集中解答
以下整理了 Stack Overflow、掘金、CSDN 等平台高频提问,按出现频率排序:
| 问题 | 解决方案 |
|---|---|
@Cacheable 不生效? |
① 检查是否开启 @EnableCaching;② 检查方法是否为 public; ③ 检查是否跨类调用(需通过代理)。 |
| 如何实现缓存预热? | 启动时通过 CommandLineRunner 或 ApplicationRunner 加载热点数据至 Redis。 |
| 缓存 Key 过长导致性能下降? | 使用 MD5/HASH 算法压缩 Key; 或自定义 KeyGenerator 生成短 Key。 |
| Redis 连接池配置不当? | Lettuce 默认使用单连接,生产环境建议改用 lettuce.pool.max-active 配置连接池。 |
| 如何清除指定缓存? | 使用 @CacheEvict(allEntries = true) 清空整表缓存;或通过 cacheManager.getCache("name").clear() 精准清除。 |
Spring 缓存最佳实践(2024 最新)
根据 阿里、腾讯、字节等大厂内部规范,总结以下 7 条黄金准则:
① 缓存命名规范
采用 业务:模块:场景 格式,如:user:profile:detail、order:count:today。
② TTL 设置原则
| 场景 | TTL 推荐值 | 说明 |
|---|---|---|
| 用户信息 | 30~60 分钟 | 结合用户活跃度动态调整 |
| 商品详情 | 10~30 分钟 | 配合消息队列异步更新 |
| 配置中心 | 永不过期 | 通过配置中心推送刷新 |
③ 避免大 Key 与热 Key
- 单 Key 值不超过 10KB;
- 大对象拆分为多 Key(如分片存储);
- 热 Key 使用本地缓存 + 二级缓存兜底。
④ 异步更新策略
写操作后通过消息队列异步更新缓存,降低数据库压力:
@CacheEvict(value = "user", key = "#user.id")
public void updateUser(User user) {
userMapper.update(user);
// 发送消息队列异步更新 Redis
kafkaTemplate.send("user-cache-update", user.getId());
}
⑤ 监控与告警
关键指标:
- 缓存命中率(目标 ≥95%);
- Redis 连接数与 QPS;
- 缓存操作耗时(P99 <10ms)。
⑥ 降级方案
缓存故障时:
- 自动切换至数据库直读;
可结合 Sentinel 实现:
@SentinelResource(value = "getUser", blockHandler = "fallback")
public User getUser(Long id) {
return userMapper.selectById(id);
}
public User fallback(Long id, BlockException ex) {
// 降级逻辑:返回默认值或空对象
return new User(id, "fallback");
}
⑦ 多级缓存架构
推荐架构:
[客户端] → [Nginx 本地缓存] → [Caffeine 本地缓存] → [Redis 分布式缓存] → [DB]
优点:
- 减少 Redis 压力;
- 提升响应速度;
- 增强系统容错能力。