bigywater.tech
Spring · 26

@Cacheable:Key 生成、同步加载与自调用

@Cacheable 的键是怎么生成的?会不会不同方法互相冲突?null 会不会被缓存?并发未命中时会不会重复加载?异常时 @CacheEvict 还清不清?这篇在 Spring Framework 7.0.9 上逐项验证。

8 对 1
8 个线程并发取同一个键,没有 sync 与 sync = true 下目标方法执行的次数
0 次
同缓存、同参数的另一个方法 inner(1) 命中了 one(1) 的缓存,它自己的目标方法执行次数
2 次
经同一个类里的内部调用连调 2 次 @Cacheable 方法,目标方法执行的次数(缓存没生效)

键的生成

实测 · 本机执行输出
  无参数:缓存里的键 [SimpleKey []]
  一个参数 one(7):键 [7](键的类型 Integer)
  两个参数 two(1, "x"):键 [SimpleKey [1, x]]
  同一个缓存 "a" 里同时放无参方法和 one(7) 的结果 → 键 [7, SimpleKey []]
  key="#x + ':' + #y" 的 spel(2, "y"):键 [2:y]
  key="#root.methodName + #id" 的 named(3):键 [named3]
  键冲突:one(1) 先调用(目标方法执行 1 次),缓存 "a" 里键 1 对应 v1;再调另一个方法 inner(1)(同一个缓存 "a"、同样的单个参数 1):inner 的目标方法执行 0 次,返回的是 v1(拿到了 one(1) 的缓存值)

默认键由参数决定,不含方法名;可以用 SpEL 自定义。

null 与条件

实测 · 本机执行输出
  返回 null 的方法,没有 unless:同一个调用连续 2 次,目标方法执行 1 次;缓存里键 [0];有 unless="#result == null":同一个调用连续 2 次,目标方法执行 2 次(null 不缓存)
  condition="#id > 10":id=5 → 同一个调用连续 2 次,目标方法执行 2 次;id=50 → 同一个调用连续 2 次,目标方法执行 1 次

null 默认也被缓存,unless 与 condition 控制是否缓存。

并发加载

实测 · 本机执行输出
  没有 sync:8 个线程同时取同一个 key(加载要 200 ms),目标方法执行了 8 次,全部返回花了 207 ms
  sync = true:8 个线程同时取同一个 key(加载要 200 ms),目标方法执行了 1 次,全部返回花了 203 ms

sync = true 让并发未命中的请求只加载一次。

更新与清除

实测 · 本机执行输出
  one(5) 缓存后 @CachePut put(5):缓存值 put5(@CachePut 总会执行方法并覆盖缓存)
  @CacheEvict 之后 键 []
  allEntries 之后 键 []
  方法抛异常时 @CacheEvict(默认 beforeInvocation=false):evictFails(8) 之后键 [8](缓存没清掉)
  beforeInvocation = true:evictFailsBefore(8) 之后键 []

@CachePut 总会执行并覆盖,@CacheEvict 在方法抛异常时默认不清除。

引用

实测 · 本机执行输出
  缓存了一个可变 List:第一次拿到后往里 add 一个元素,第二次拿到 [a, b, 被调用者改过](同一个对象:true;目标方法执行 1 次)

进程内缓存存的是引用,调用方改动会污染缓存。

自调用

实测 · 本机执行输出
  通过 callInner(同一个类里调 inner)连调 2 次:目标方法执行 2 次(缓存没生效);直接调 inner 连调 2 次:目标方法执行 1 次(第 1 次未命中,第 2 次命中)

内部调用不经过代理,缓存不生效。

分步演示

@Cacheable
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用不带 Boot 的 Spring Framework 7.0.9 的 @EnableCaching 加 ConcurrentMapCacheManager(进程内);Redis 等外部缓存的序列化与过期(见后面的 Spring Cache + Redis 篇)没有测;并发实验的 200 ms 是 Thread.sleep 模拟的加载时间
    • 没有验证:Caffeine 等其它 CacheManager 的行为;缓存过期;自定义 KeyGenerator;@Caching 组合注解;@CacheConfig;事务与缓存的先后顺序;AOP 代理类型对缓存的影响;sync = true 时的异常传播
    京ICP备2026067502号-1京公网安备11010502063487号