Spring 缓存体系全景概览

在现代 Java 企业级应用开发中,Spring 三级缓存是系统性能优化与高并发场景下的关键组件。Spring 框架通过 Cache 抽象层,实现了统一的缓存操作接口,并支持多种缓存实现(如 ConcurrentHashMap、Ehcache、Redis、Caffeine 等)。其中,三级缓存机制并非指 Spring 自身的三类缓存实现,而是指在 Spring 框架中,缓存分层设计与调用链路所形成的逻辑三层结构:

在 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 为例,其执行流程如下:

  1. 方法调用前,CacheInterceptor 拦截并解析缓存名称(cacheNames);
  2. 通过 CacheResolver 获取对应 Cache 实例;
  3. 调用 KeyGenerator 生成缓存 Key;
  4. 尝试从 Cache 中获取值:
    • 命中缓存 → 直接返回缓存值,跳过原方法;
    • 未命中 → 执行原方法,将结果存入缓存后返回。

该流程可表示为伪代码:

if (cache.containsKey(key)) {
    return cache.get(key);
}
Object result = method.invoke();
cache.put(key, result);
return result;

2. 缓存键(Key)生成策略详解

默认 SimpleKeyGenerator 使用以下规则生成 Key:

但该策略在参数为对象时存在风险(未实现 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 压力;
  • 提升响应速度;
  • 增强系统容错能力。