接口太慢?Spring Boot 缓存体系 @Cacheable 全链路拆解

接口太慢?Spring Boot 缓存体系 @Cacheable 全链路拆解

每次查接口都要 200ms,加了缓存直接降到 2ms。但 @Cacheable 到底是怎么工作的?缓存什么时候生效、什么时候失效?多级缓存怎么搭?这篇把 Spring Boot 缓存体系从注解到源码一次性拆透。


一、问题:为什么需要缓存

一个典型的商品详情接口,每次请求都要:

markdown 复制代码
用户请求 → 查商品基本信息 → 查 SKU 列表 → 查库存 → 查价格策略 → 查评价摘要
         ↓                  ↓              ↓          ↓              ↓
       DB 20ms           DB 30ms       DB 15ms    DB 20ms       DB 25ms
                                                         总计 ≈ 110ms

加上网络开销,接口响应 200ms+。但商品信息一天才变几次?99% 的请求都在重复查数据库。

缓存的核心思路:第一次查完把结果存起来,后续请求直接返回存储的结果,跳过数据库。

css 复制代码
用户请求 → [缓存层] → 命中?→ 直接返回(2ms)
              ↓ 未命中
           数据库查询 → 写入缓存 → 返回(110ms)

Spring Boot 提供了一套完整的缓存抽象,核心是三个注解:@Cacheable@CachePut@CacheEvict


二、三大核心注解

2.1 @Cacheable:查缓存,没有就查库并写入

java 复制代码
@Service
public class ProductService {

    @Cacheable(value = "product", key = "#id")
    public Product getProductById(Long id) {
        // 只有缓存未命中时才执行这个方法
        return productMapper.selectById(id);
    }
}

执行流程:

scss 复制代码
调用 getProductById(1)
        ↓
┌─────────────────────────────────┐
│  1. 生成 Key: product::1       │
│  2. 查缓存                      │
│  3. 命中?→ 返回缓存值,方法不执行  │
│  4. 未命中?→ 执行方法            │
│  5. 将返回值写入缓存              │
│  6. 返回结果                     │
└─────────────────────────────────┘

2.2 @CachePut:总是执行方法,更新缓存

java 复制代码
@CachePut(value = "product", key = "#product.id")
public Product updateProduct(Product product) {
    productMapper.updateById(product);
    return product; // 返回值会覆盖缓存
}
对比 @Cacheable @CachePut
方法是否执行 缓存命中时不执行 总是执行
缓存操作 读 + 写(按需) 只写(覆盖)
典型场景 查询方法 更新方法

2.3 @CacheEvict:删除缓存

java 复制代码
@CacheEvict(value = "product", key = "#id")
public void deleteProduct(Long id) {
    productMapper.deleteById(id);
}

// 清除整个 product 缓存空间的所有 key
@CacheEvict(value = "product", allEntries = true)
public void refreshAllProducts() {
    // 批量更新后清空整个缓存
}

2.4 组合使用

java 复制代码
@Service
public class ProductService {

    @Cacheable(value = "product", key = "#id")
    public Product getProductById(Long id) {
        return productMapper.selectById(id);
    }

    @CachePut(value = "product", key = "#product.id")
    public Product updateProduct(Product product) {
        productMapper.updateById(product);
        return product;
    }

    @CacheEvict(value = "product", key = "#id")
    public void deleteProduct(Long id) {
        productMapper.deleteById(id);
    }
}

三、缓存抽象层:CacheManager SPI

Spring 不绑定具体缓存实现,而是定义了 CacheManager 接口:

scss 复制代码
┌─────────────────────────────────────────────┐
│              Spring 缓存抽象层                 │
│  CacheOperations  ← 注解驱动                  │
│  CacheManager     ← SPI 接口                  │
│  Cache            ← 缓存操作接口               │
└───────────────────┬─────────────────────────┘
                    │ 实现
    ┌───────────────┼───────────────┐
    │               │               │
    ▼               ▼               ▼
┌────────┐  ┌────────────┐  ┌───────────┐
│Caffeine│  │   Redis    │  │  EhCache  │
│(本地)  │  │ (分布式)   │  │ (本地/分布式)│
└────────┘  └────────────┘  └───────────┘

3.1 常用缓存实现对比

维度 Caffeine Redis EhCache
类型 本地缓存 分布式缓存 本地/分布式
性能 极高(纳秒级) 高(毫秒级,受网络影响)
容量 受 JVM 内存限制 受服务器内存限制 可持久化到磁盘
过期策略 W-TinyLFU + 时间 TTL + LRU LRU + 时间
多实例同步 不支持 天然支持 支持(集群模式)
适用场景 单机、读多写少 分布式、跨实例共享 大数据量本地缓存

3.2 引入 Caffeine(本地缓存)

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
java 复制代码
@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(30, TimeUnit.MINUTES)  // 写入后 30 分钟过期
                .expireAfterAccess(10, TimeUnit.MINUTES) // 最后一次访问后 10 分钟过期
                .initialCapacity(100)                    // 初始容量
                .maximumSize(1000));                     // 最大缓存条目
        return cacheManager;
    }
}

3.3 引入 Redis(分布式缓存)

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yaml 复制代码
spring:
  cache:
    type: redis
    redis:
      time-to-live: 30m       # 默认过期时间
      cache-null-values: false # 是否缓存 null 值
      key-prefix: "myapp::"   # key 前缀
      use-key-prefix: true
  redis:
    host: 127.0.0.1
    port: 6379

四、@Cacheable 源码拆解

Spring 缓存的本质是 AOP。当调用 @Cacheable 方法时,实际调用的是代理对象,经过 CacheInterceptor 拦截后才到目标方法。

4.1 AOP 拦截链路

scss 复制代码
调用方
  ↓
代理对象 (Proxy)
  ↓
CacheInterceptor.invoke()          ← AOP 拦截器
  ↓
CacheAspectSupport.execute()       ← 核心缓存逻辑
  ↓
目标方法 (getProductById)
  ↓
返回结果

4.2 CacheInterceptor 源码骨架

java 复制代码
// 缓存拦截器,继承自 CacheAspectSupport
public class CacheInterceptor extends CacheAspectSupport 
    implements MethodInterceptor, Serializable {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();
        CacheOperationInvoker aopAllianceInvoker = () -> {
            try {
                return invocation.proceed(); // 执行目标方法
            } catch (Throwable ex) {
                throw new ThrowableWrapper(ex);
            }
        };

        // 调用父类的 execute 方法,处理缓存逻辑
        return execute(aopAllianceInvoker, invocation.getThis(), method, 
                       invocation.getArguments());
    }
}

4.3 CacheAspectSupport.execute 核心流程

java 复制代码
public Object execute(CacheOperationInvoker invoker, Object target, 
                      Method method, Object[] args) {
    
    // 1. 解析方法上的缓存注解,获取 CacheOperation
    Collection<CacheOperation> operations = getOperations(method, targetClass);
    
    // 2. 创建 CacheOperationContexts(封装所有操作的上下文)
    CacheOperationContexts contexts = new CacheOperationContexts(operations, args, target, method);
    
    // 3. 处理 @Cacheable ------ 查缓存
    Object result = processCacheable(contexts, invoker);
    
    // 4. 处理 @CachePut ------ 更新缓存
    processCachePut(contexts, result);
    
    // 5. 处理 @CacheEvict ------ 删除缓存
    processCacheEvict(contexts, result, true);  // beforeInvocation=false
    processCacheEvict(contexts, result, false); // beforeInvocation=true
    
    return result;
}

4.4 @Cacheable 核心判断逻辑

java 复制代码
private Object processCacheable(CacheOperationContexts contexts, 
                                 CacheOperationInvoker invoker) {
    
    for (CacheOperationContext context : contexts.get(CacheableOperation.class)) {
        
        // 1. 生成 cache key
        Object key = generateKey(context);
        
        // 2. 遍历所有缓存名称
        for (Cache cache : context.getCaches()) {
            
            // 3. 查缓存
            Optional<Object> cachedValue = cache.get(key);
            
            if (cachedValue.isPresent()) {
                // 4. 命中 → 直接返回,不执行方法
                return cachedValue.get();
            }
        }
    }
    
    // 5. 未命中 → 执行目标方法
    Object result = invoker.invoke();
    
    // 6. 将结果写入所有缓存
    for (CacheOperationContext context : contexts.get(CacheableOperation.class)) {
        Object key = generateKey(context);
        for (Cache cache : context.getCaches()) {
            cache.put(key, result);
        }
    }
    
    return result;
}

4.5 Key 生成机制

java 复制代码
// 默认使用 SimpleKeyGenerator
public class SimpleKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object target, Method method, Object... params) {
        if (params.length == 0) {
            return SimpleKey.EMPTY;          // 无参数 → SimpleKey.EMPTY
        }
        if (params.length == 1) {
            return params[0];                // 单参数 → 直接用参数值
        }
        return new SimpleKey(params);         // 多参数 → SimpleKey 包裹
    }
}

Key 格式:{cacheName}::{key}

注解配置 生成的 Key
@Cacheable(value="product", key="#id") product::1
@Cacheable(value="product", key="#p0") product::1
@Cacheable(value="product")(无 key) product::SimpleKey[1]
@Cacheable(value="product", key="#user.id + '_' + #type") product::1001_VIP

4.6 自定义 Key 生成器

java 复制代码
@Component
public class CustomKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object target, Method method, Object... params) {
        StringBuilder sb = new StringBuilder();
        sb.append(target.getClass().getSimpleName()).append(":");
        sb.append(method.getName()).append(":");
        for (Object param : params) {
            sb.append(param != null ? param.toString() : "null").append(":");
        }
        return sb.toString().hashCode();
    }
}

// 使用
@Cacheable(value = "product", keyGenerator = "customKeyGenerator")
public Product getProductById(Long id) {
    return productMapper.selectById(id);
}

五、条件缓存

5.1 condition vs unless

java 复制代码
// condition:方法执行前判断,满足条件才查/写缓存
@Cacheable(value = "product", key = "#id", condition = "#id > 0")
public Product getProductById(Long id) {
    return productMapper.selectById(id);
}

// unless:方法执行后判断,满足条件不写入缓存
@Cacheable(value = "product", key = "#id", unless = "#result == null")
public Product getProductById(Long id) {
    return productMapper.selectById(id);
}

// 组合使用:id > 0 才查缓存,结果为 null 不写入缓存
@Cacheable(value = "product", key = "#id", 
           condition = "#id > 0", 
           unless = "#result == null || #result.stock == 0")
public Product getProductById(Long id) {
    return productMapper.selectById(id);
}
维度 condition unless
判断时机 方法执行前 方法执行后
影响 是否查/写缓存 是否写入缓存
能否用到 result 不能
典型场景 参数过滤 过滤空值/异常值

5.2 SpEL 上下文变量

变量 说明 示例
#id 方法参数名 key="#id"
#p0 方法参数索引 key="#p0"
#root.methodName 方法名 condition="#root.methodName=='getXxx'"
#root.target 目标对象
#result 返回值(仅 unless 可用) unless="#result==null"

六、多级缓存架构

单层缓存在分布式场景下有局限:本地缓存无法跨实例共享,分布式缓存每次都要网络请求。多级缓存可以兼顾两者优势。

yaml 复制代码
┌────────────────────────────────────────────────────┐
│                    请求入口                         │
└────────────────────┬───────────────────────────────┘
                     ▼
            ┌─────────────────┐
            │  L1: 本地缓存    │ ← Caffeine (纳秒级)
            │  (进程内)        │
            └────────┬────────┘
                     │ 未命中
                     ▼
            ┌─────────────────┐
            │  L2: 分布式缓存  │ ← Redis (毫秒级)
            │  (跨实例共享)    │
            └────────┬────────┘
                     │ 未命中
                     ▼
            ┌─────────────────┐
            │  数据库          │ ← MySQL (十毫秒级)
            └─────────────────┘

6.1 自定义多级 CacheManager

java 复制代码
public class MultiLevelCacheManager implements CacheManager {

    private final CacheManager localCache;   // Caffeine
    private final CacheManager remoteCache;  // Redis

    public MultiLevelCacheManager(CacheManager local, CacheManager remote) {
        this.localCache = local;
        this.remoteCache = remote;
    }

    @Override
    public Cache getCache(String name) {
        Cache local = localCache.getCache(name);
        Cache remote = remoteCache.getCache(name);
        return new MultiLevelCache(local, remote);
    }

    @Override
    public Collection<String> getCacheNames() {
        return localCache.getCacheNames();
    }
}

6.2 MultiLevelCache 实现

java 复制代码
public class MultiLevelCache implements Cache {

    private final Cache localCache;
    private final Cache remoteCache;

    @Override
    public ValueWrapper get(Object key) {
        // 1. 先查 L1 本地缓存
        ValueWrapper value = localCache.get(key);
        if (value != null) {
            return value; // L1 命中
        }

        // 2. L1 未命中,查 L2 分布式缓存
        value = remoteCache.get(key);
        if (value != null) {
            // 3. 回填 L1
            localCache.put(key, value.get());
            return value;
        }

        return null; // 都未命中
    }

    @Override
    public void put(Object key, Object value) {
        // 同时写入 L1 和 L2
        localCache.put(key, value);
        remoteCache.put(key, value);
    }

    @Override
    public void evict(Object key) {
        // 同时清除 L1 和 L2
        localCache.evict(key);
        remoteCache.evict(key);
    }
}

6.3 多级缓存效果对比

场景 无缓存 仅 L1 (Caffeine) 仅 L2 (Redis) L1 + L2
首次请求 110ms 110ms 110ms 110ms
同实例重复 110ms 0.01ms 2ms 0.01ms
跨实例重复 110ms 110ms 2ms 2ms
数据更新 - 需广播清除 自动清除 自动清除 L2

七、缓存三大经典问题

7.1 缓存穿透

问题:查询一定不存在的数据,缓存永远不会命中,每次都查数据库。

ini 复制代码
请求(id=-1) → 缓存未命中 → DB 查不到 → 不写缓存 → 下次还是穿透

解法:缓存空值

java 复制代码
@Cacheable(value = "product", key = "#id", unless = "#result == null")
public Product getProductById(Long id) {
    Product product = productMapper.selectById(id);
    if (product == null) {
        // 返回一个空对象标记,而不是 null
        // unless 条件中 #result == null 会让 null 不被缓存
        // 这里可以改为缓存空标记
    }
    return product;
}

// 或者用布隆过滤器
public Product getProductById(Long id) {
    // 布隆过滤器先判断是否存在
    if (!bloomFilter.mightContain(id)) {
        return null; // 一定不存在,直接返回
    }
    return getProductFromCache(id);
}

7.2 缓存雪崩

问题:大量缓存同时过期,请求全部打到数据库。

解法:过期时间加随机值

java 复制代码
@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(Caffeine.newBuilder()
                // 过期时间 = 基础时间 + 随机偏移,避免同时过期
                .expireAfterWrite(Duration.ofMinutes(25 + ThreadLocalRandom.current().nextInt(10)))
                .maximumSize(10000));
        return manager;
    }
}

7.3 缓存击穿

问题:一个热点 key 过期瞬间,大量并发请求同时查数据库。

解法:加锁,只让一个请求查数据库

java 复制代码
@Cacheable(value = "product", key = "#id")
public Product getProductById(Long id) {
    // Spring Cache 本身不加锁
    return productMapper.selectById(id);
}

// 手动加锁
public Product getProductWithLock(Long id) {
    String cacheKey = "product:" + id;
    Product product = redisTemplate.opsForValue().get(cacheKey);
    
    if (product != null) {
        return product;
    }
    
    // 双重检查锁
    synchronized (this) {
        product = redisTemplate.opsForValue().get(cacheKey);
        if (product != null) {
            return product;
        }
        
        product = productMapper.selectById(id);
        redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
        return product;
    }
}

7.4 三大问题对比

问题 触发条件 根因 解法
穿透 查询不存在的数据 缓存没存空值 缓存空值/布隆过滤器
雪崩 大量缓存同时过期 过期时间相同 随机过期时间
击穿 热点 key 过期瞬间 并发查库无锁 互斥锁/永不过期

八、序列化踩坑

8.1 Redis 缓存序列化问题

Spring Boot 默认使用 JDK 序列化,存入 Redis 的是二进制,可读性差且跨语言不兼容。

java 复制代码
@Configuration
public class RedisCacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        // 自定义序列化配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .serializeKeysWith(
                    RedisSerializationContext.SerializationPair.fromSerializer(
                        new StringRedisSerializer()))
                .serializeValuesWith(
                    RedisSerializationContext.SerializationPair.fromSerializer(
                        new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues();

        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }
}

8.2 序列化方案对比

序列化方式 可读性 性能 跨语言 体积
JDK
JSON(Jackson)
Protobuf 最小
Hessian

8.3 实体类必须实现 Serializable

java 复制代码
// 如果使用 JDK 序列化,实体类必须实现 Serializable
public class Product implements Serializable {
    private Long id;
    private String name;
    private BigDecimal price;
    // ...
}

// JSON 序列化不需要,但建议加默认构造方法
public class Product {
    private Long id;
    private String name;
    private BigDecimal price;

    // 必须有无参构造方法
    public Product() {}
    
    // getter/setter...
}

九、@Caching 组合注解

需要同时执行多个缓存操作时,用 @Caching

java 复制代码
@Caching(
    put = {
        @CachePut(value = "product", key = "#product.id"),
        @CachePut(value = "product:detail", key = "#product.id + ':detail'"),
        @CachePut(value = "product:summary", key = "#product.categoryId + ':summary'")
    }
)
public Product updateProduct(Product product) {
    productMapper.updateById(product);
    return product;
}

9.1 自定义注解简化

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Caching(
    cacheable = {
        @Cacheable(value = "product", key = "#id")
    },
    put = {
        @CachePut(value = "product:detail", key = "#id + ':detail'", condition = "#result != null")
    }
)
public @interface CacheableProduct {
}
java 复制代码
@CacheableProduct
public Product getProductById(Long id) {
    return productMapper.selectById(id);
}

十、缓存监控与指标

10.1 Caffeine 内置统计

java 复制代码
@Bean
public CacheManager cacheManager() {
    CaffeineCacheManager manager = new CaffeineCacheManager();
    manager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .recordStats()); // 开启统计
    return manager;
}

// 查看统计信息
@Autowired
private CacheManager cacheManager;

public void printCacheStats() {
    CaffeineCache cache = (CaffeineCache) cacheManager.getCache("product");
    CacheStats stats = cache.getNativeCache().stats();
    
    log.info("命中率: {}", stats.hitRate());
    log.info("命中次数: {}", stats.hitCount());
    log.info("未命中次数: {}", stats.missCount());
    log.info("加载次数: {}", stats.loadCount());
    log.info("加载总耗时: {}ms", stats.totalLoadTime() / 1_000_000);
    log.info("驱逐次数: {}", stats.evictionCount());
}

10.2 典型监控指标

指标 含义 关注点
hitRate 命中率 低于 80% 需排查
missCount 未命中次数 突增可能有穿透
evictionCount 驱逐次数 频繁驱逐说明容量不够
totalLoadTime 加载耗时 判断缓存回源性能
averageLoadTime 平均加载时间 判断 DB 查询性能

十一、总结:Spring 缓存体系全景

less 复制代码
┌─────────────────────────────────────────────────────────┐
│                     开发者视角                             │
│  @Cacheable  @CachePut  @CacheEvict  @Caching            │
│         │          │          │           │               │
│         └──────────┴──────────┴───────────┘              │
│                        ↓                                 │
│              Spring Cache Abstraction                    │
│         (CacheManager / Cache / KeyGenerator)            │
│                        ↓                                 │
│         ┌──────────────┼──────────────┐                  │
│         │              │              │                  │
│         ▼              ▼              ▼                  │
│    Caffeine        Redis         EhCache                │
│    (本地)         (分布式)       (混合)                 │
│         │              │              │                  │
│         └──────────────┴──────────────┘                  │
│                        ↓                                 │
│              CacheInterceptor (AOP)                      │
│         1. 解析注解  2. 生成Key  3. 查缓存               │
│         4. 执行方法  5. 写缓存   6. 清缓存               │
└─────────────────────────────────────────────────────────┘

核心要点:

要点 说明
注解驱动 @Cacheable 查缓存,@CachePut 更新缓存,@CacheEvict 清除缓存
AOP 拦截 缓存逻辑通过 CacheInterceptor 在方法调用前/后介入
CacheManager SPI 解耦缓存实现,切换 Caffeine/Redis 只需改配置
Key 生成 默认 SimpleKeyGenerator,支持 SpEL 和自定义 KeyGenerator
条件缓存 condition 方法前判断,unless 方法后判断
多级缓存 本地缓存 + 分布式缓存,兼顾性能和一致性
三大问题 穿透 → 缓存空值,雪崩 → 随机过期,击穿 → 互斥锁
序列化 Redis 推荐用 JSON 序列化,注意实体类与构造方法

下一篇,我们会拆解 Spring Boot 定时任务体系,从 @Scheduled 注解到分布式调度的全链路。

相关推荐
snow@li2 小时前
SpringBoot:AOP日志切面全景梳理/原理+流程+实战+避坑
java·开发语言·spring boot
snow@li4 小时前
SpringBoot:全套生命周期全景详解/应用级+Bean级
java·spring boot·rpc
凤山老林17 小时前
Spring Boot @Async 线上实战:从默认配置到生产级线程池治理
java·spring boot·后端
凤山老林21 小时前
Spring Boot 定时任务进阶:动态 Cron 与集群防重实战
java·spring boot·后端·定时任务·集群定时任务
凤山老林1 天前
Spring Boot 配置管理实战:多环境隔离、加密与 Nacos 热更新避坑指南
java·spring boot·后端
凤山老林1 天前
可观测性落地:Spring Boot 3.x + Actuator + Prometheus + Grafana 监控体系搭建
spring boot·grafana·prometheus
agent8971 天前
实战升级|SpringBoot WebSocket实现多轮对话AI流式问答(上下文记忆+自动重连+会话隔离)
人工智能·spring boot·websocket
白仑色1 天前
Spring Boot 从切面统一控制事务
java·spring boot·aop·spring事务
凤山老林1 天前
Spring Boot 大文件处理实战:分片上传、断点续传与 OSS 集成
java·spring boot·后端·大文件上传·分片上传·断点续传