← 返回题目列表

Spring Cache(@Cacheable)是怎么工作的?它和具体的缓存实现(Redis/Caffeine)是什么关系?

中等 第 26 / 30 题 更新于 2026/07/28
Spring CacheCacheable缓存抽象AOP

简化版

Spring Cache 是一套「缓存抽象」——它用 @Cacheable/@CachePut/@CacheEvict 等注解,让你声明式地给方法加缓存,而不用关心底层用的是哪种缓存(Redis、Caffeine、EhCache 还是本地 Map)怎么工作:本质是 AOP——给标了 @Cacheable 的方法织入一个「环绕通知」,方法调用时:先按 key 去缓存查,命中就直接返回缓存值、不执行方法(省掉查库);没命中才执行方法,把返回值存进缓存再返回。和具体缓存实现的关系:Spring Cache 只是「抽象层」,定义了 CacheCacheManager 两个接口;真正存哪、怎么存由「缓存提供者」决定——引入 spring-boot-starter-cache + Redis,CacheManager 就是 RedisCacheManager(存 Redis);引入 Caffeine,就是本地内存缓存。换缓存实现只需换依赖和配置,注解不用动——这就是「抽象」的价值:面向注解编程,屏蔽底层缓存差异。

详细版

核心注解

注解作用行为
@Cacheable查缓存命中则返回缓存、不执行方法;未命中执行方法并缓存结果
@CachePut更新缓存总是执行方法,把结果写入缓存(用于更新场景)
@CacheEvict删缓存执行方法后删除缓存条目(allEntries=true 清空)
@Caching组合一个方法上组合多个缓存操作
@CacheConfig类级配置类上统一配 cacheNames 等

@Cacheable 的执行逻辑

调用 getUser(1) —— 方法上有 @Cacheable(value="users", key="#id")

① 用 keyGenerator/SpEL 算出 key(这里 key = 1)
② 去 CacheManager 的 "users" 这个 Cache 里查 key=1

命中?── 是 → 直接返回缓存值,方法体不执行(省掉查库)
      └ 否 → 执行方法体(查库)→ 把返回值存入缓存 → 返回
@Service
public class UserService {
    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public User getUser(Long id) {           // 查:命中不执行、未命中查库并缓存
        return userMapper.selectById(id);
    }
    @CachePut(value = "users", key = "#user.id")
    public User update(User user) {          // 改:总执行,结果写入缓存
        userMapper.update(user); return user;
    }
    @CacheEvict(value = "users", key = "#id")
    public void delete(Long id) {            // 删:执行后清缓存
        userMapper.deleteById(id);
    }
}

⚠️ Spring Cache 是 AOP 实现的,所以「AOP 失效场景」它全中招:① 同类内部自调用(一个方法调本类的 @Cacheable 方法)不走代理,缓存注解失效;② @Cacheable 方法必须是 public、通过代理对象调用才生效。另外几个易错点:unless="#result==null" 常用来「结果为 null 就不缓存」(否则缓存空值可能不是你想要的,但要防缓存穿透时又可能故意缓存空值);@CachePut@Cacheable 区别——@CachePut 总会执行方法(用于「改完同步缓存」),@Cacheable 命中就跳过方法;key 用 SpEL#id 引用参数),写成 key="id" 会变成固定字符串导致所有调用共享一个缓存条目。

完整版教学

一、Spring Cache 是「抽象」不是「实现」

理解 Spring Cache 的第一要点:它是「抽象层」,本身不存数据:

Spring Cache 提供的是"缓存的编程模型"(注解 + 接口):
  - 注解:@Cacheable / @CachePut / @CacheEvict
  - 接口:Cache(一块缓存的抽象)、CacheManager(管理多个 Cache)

它本身不实现"数据存哪"——那是"缓存提供者"的事:
  Redis      → RedisCacheManager(存 Redis)
  Caffeine   → CaffeineCacheManager(本地内存)
  EhCache    → EhCacheCacheManager
  简单 Map   → ConcurrentMapCacheManager(默认,仅测试用)

类比 JDBC:
  Spring Cache ≈ JDBC 接口(统一 API)
  Redis/Caffeine ≈ 具体数据库驱动(真正干活)
  → 面向抽象编程,换实现不改业务代码

Spring Cache 是「缓存的抽象层」——它定义注解(@Cacheable 等)和接口(Cache/CacheManager),但不实现「数据存哪」,那由「缓存提供者」(Redis/Caffeine/EhCache)实现。这和 JDBC 一样:Spring Cache 是统一 API,具体缓存是「驱动」。价值是「面向抽象编程,换缓存实现不改业务代码」。理解「Spring Cache 是抽象层(注解+Cache/CacheManager 接口)、不实现存储、由缓存提供者实现、类比 JDBC」,就抓住了它的本质。

二、CacheManager 与 Cache:两个核心接口

Spring Cache 抽象的核心是两个接口:

Cache(一块命名缓存的抽象):
  get(key) / put(key, value) / evict(key) / clear()
  → 代表一个具体的缓存区域(如 "users" 缓存、"products" 缓存)

CacheManager(缓存管理器,管多个 Cache):
  getCache(name)  → 按名字拿到一个 Cache
  → @Cacheable(value="users") 里的 "users" 就是 Cache 的名字
    CacheManager.getCache("users") 拿到这块缓存

不同提供者的实现:
  RedisCacheManager.getCache("users")
    → 返回一个 RedisCache,底层操作 Redis
  CaffeineCacheManager.getCache("users")
    → 返回一个 CaffeineCache,底层是本地 Caffeine 缓存

所以:@Cacheable 注解 → 找 CacheManager → getCache(名字)
     → 在这个 Cache 上 get/put → 底层落到 Redis/Caffeine

两个核心接口:Cache(一块命名缓存,get/put/evict);CacheManager(管多个 Cache,getCache(name) 按名字取)。@Cacheable(value="users") 里的 "users" 就是 Cache 的名字——注解触发时,从 CacheManager 取名为 “users” 的 Cache,在上面 get/put,底层落到 Redis/Caffeine。不同提供者提供不同的 CacheManager/Cache 实现。理解「Cache 是一块命名缓存(get/put)、CacheManager 管多个 Cache(getCache 按名取)、@Cacheable 的 value 是 Cache 名、不同提供者不同实现」,就理解了抽象的核心结构。

三、@Cacheable 的 AOP 工作原理

Spring Cache 靠 AOP 工作——理解这点就理解了它的一切行为和坑:

@Cacheable 本质是给方法织入一个"环绕通知"(Around Advice):

代理对象拦截 getUser(1) 调用:
  1. 前置:算 key(SpEL #id → 1),去 Cache 查
  2. 命中 → 直接返回缓存值,★根本不执行原方法(不查库)
  3. 未命中 → 执行原方法(查库)→ 拿到结果 → put 进 Cache → 返回

关键推论(因为是 AOP 代理):
  - 只有"通过代理对象"调用才生效
  - 同类内部自调用(this.getUser())不走代理 → 缓存失效
  - private/final 方法无法被代理 → 注解无效
  - 和 @Transactional、@Async 是同一类 AOP 失效问题

Spring Cache 的引擎是 AOP——@Cacheable 给方法织入「环绕通知」:调用时先查缓存,命中直接返回不执行原方法,未命中才执行并缓存。这解释了它的所有行为和坑:因为是代理,同类自调用(this.method())不走代理导致缓存失效、private/final 方法无法代理。它和 @Transactional@Async同一类 AOP 失效问题(前面 AOP 题讲过自调用)。理解「@Cacheable 靠 AOP 环绕通知、命中不执行原方法、同类自调用失效(和 @Transactional 同类问题)」,就理解了它的工作原理和失效根源。

四、三大注解的区别与配合

@Cacheable@CachePut@CacheEvict 分工不同,配合使用保证「缓存与数据库一致」:

@Cacheable(读):命中就返回、不执行方法;未命中执行并缓存
  用于:查询方法(getUser)

@CachePut(写):★总是执行方法,然后把结果写入缓存
  用于:更新方法(updateUser)——改完库,顺便把新值同步进缓存
  区别 @Cacheable:@Cacheable 命中会跳过方法,@CachePut 从不跳过

@CacheEvict(删):执行方法后删除缓存条目
  用于:删除方法(deleteUser)——删库后把缓存也清掉
  allEntries=true:清空整个 Cache
  beforeInvocation=true:方法执行前就删(防方法异常导致没删成)

配合保证一致性(典型「更新数据库 + 维护缓存」):
  查 → @Cacheable
  改 → @CachePut(同步新值)或 @CacheEvict(直接删,下次查重新加载)
  删 → @CacheEvict

三大注解分工:@Cacheable(读,命中跳过方法)、@CachePut(写,总执行方法并更新缓存)、@CacheEvict(删,清除缓存)。关键区别:@CachePut 从不跳过方法(用于更新时同步缓存),@Cacheable 命中就跳过。更新数据时有两种缓存策略:@CachePut 同步新值,或 @CacheEvict 直接删(下次查重新加载,更常用更安全)。@CacheEvictbeforeInvocation 控制删除时机。理解「@Cacheable 读跳过/@CachePut 写总执行/@CacheEvict 删、更新时用 CachePut 同步或 CacheEvict 删除保一致」,就掌握了三大注解的配合。

五、key 生成与条件控制

Spring Cache 用 SpEL 灵活控制「用什么 key、什么时候缓存」:

key(缓存的键):
  key = "#id"           用方法参数 id 作 key(SpEL)
  key = "#user.id"      用参数对象的属性
  key = "#a0 + '_' + #a1"  多参数组合
  不写 key → 用 KeyGenerator 默认生成(所有参数组合)
  ★ 注意:key="id" 是字符串字面量 "id",不是参数!要写 #id

condition(满足才缓存/查缓存):
  condition = "#id > 10"   只有 id>10 才走缓存

unless(满足则不缓存结果):
  unless = "#result == null"   结果为 null 不缓存
  区别:condition 在方法执行前判断(能拿参数);
        unless 在方法执行后判断(能拿 #result 结果)

sync(并发时只让一个线程加载,防缓存击穿):
  @Cacheable(sync = true)

key 和条件控制都用 SpELkey 定缓存键(#id 引用参数,注意 key="id" 是字面量不是参数);condition 定「满足才缓存」(方法前判断,能拿参数);unless 定「满足则不缓存」(方法后判断,能拿 #result);sync=true 并发时只让一个线程加载(防缓存击穿)。这些让缓存行为可精细控制。理解「key 用 SpEL #参数(别写成字面量)、condition 方法前判断、unless 方法后判断能用#result、sync 防击穿」,就掌握了缓存的精细控制。

六、换缓存实现:抽象的价值兑现

Spring Cache 抽象的价值,在「换缓存实现」时兑现:

同样的业务代码(@Cacheable 注解不变),换底层缓存:

用本地缓存 Caffeine(单机、快、无网络开销):
  引入 caffeine 依赖 + 配置 CaffeineCacheManager
  → 缓存存 JVM 内存

用分布式缓存 Redis(多实例共享、可持久化):
  引入 spring-boot-starter-data-redis + 配置 RedisCacheManager
  → 缓存存 Redis,多个应用实例共享同一份缓存

甚至多级缓存(Caffeine + Redis):
  自定义 CacheManager,先查本地再查 Redis

★ 业务代码里的 @Cacheable/@CacheEvict 一个字都不用改!
  只改依赖和配置 → 这就是"缓存抽象"的核心价值

选择:
  单机/读多写少/能容忍短暂不一致 → 本地 Caffeine
  多实例要共享缓存/要一致 → Redis
  又要快又要共享 → 多级(本地 + Redis)

抽象的价值在「换实现不改代码」时兑现——同样的 @Cacheable 注解,换依赖和配置就能从本地 Caffeine 切到分布式 Redis,甚至组合成多级缓存,业务代码一个字不改。选择:单机读多写少用本地 Caffeine(快)、多实例共享用 Redis、又快又共享用多级。这正是「面向抽象编程」的回报。理解「换缓存实现只改依赖和配置、@Cacheable 注解不动、Caffeine(本地快)/Redis(分布式共享)/多级、抽象价值在换实现时兑现」,就理解了 Spring Cache 抽象的终极意义。

记忆钩子:「Spring Cache = 缓存抽象层(注解 @Cacheable/@CachePut/@CacheEvict + 接口 Cache/CacheManager),不实现存储、由提供者(Redis/Caffeine)实现,类比 JDBC;靠 AOP 环绕通知工作:@Cacheable 命中直接返回不执行方法、未命中执行并缓存→所以同类自调用失效(和 @Transactional 同类问题);三注解:@Cacheable 读(命中跳过)/@CachePut 写(总执行更新缓存)/@CacheEvict 删;key/condition/unless 用 SpEL(key=#id 别写字面量 id、unless 用#result、sync 防击穿);换缓存只改依赖配置、注解不动」

七、常见误区与追问

  • 误区:Spring Cache 自己会存数据。 不会——它只是抽象层(注解 + Cache/CacheManager 接口),真正存哪由缓存提供者实现(Redis/Caffeine/EhCache);不引入提供者时默认用 ConcurrentMapCacheManager(内存 Map,仅测试用)。
  • 误区:@Cacheable 加了就一定生效。 它靠 AOP 代理——同类内部自调用(this.method())不走代理会失效、private/final 方法无法代理、必须 public 且通过代理对象调用;和 @Transactional/@Async 是同一类失效问题。
  • 误区:@Cacheable 和 @CachePut 一样。 @Cacheable 命中缓存就跳过方法(读场景);@CachePut 总是执行方法再把结果写入缓存(更新场景,「改完同步缓存」);用错会导致该更新时没执行、或该缓存时重复查库。
  • 误区:key=“id” 能用方法参数 id。 key 是 SpEL,key=“id” 是字符串字面量 “id”(所有调用共用一个 key,缓存串数据);引用参数要写 key=“#id”。
  • 追问:更新数据时,缓存该用 @CachePut 还是 @CacheEvict? 两种策略:@CachePut 把新值同步进缓存(读多且更新后立刻会读时好);@CacheEvict 直接删缓存、下次查再重新加载(更简单更安全,避免 @CachePut 和缓存的复杂一致性问题)——实践中 @CacheEvict「删除」更常用。
  • 追问:condition 和 unless 有什么区别? condition 在方法执行「前」判断(只能用参数,如 #id>10 才走缓存);unless 在方法执行「后」判断(能用 #result 结果,如 #result==null 就不缓存);一个控制「要不要走缓存流程」,一个控制「拿到结果后要不要缓存」。
  • 追问:怎么防缓存击穿(热点 key 失效瞬间大量请求打到库)? @Cacheable(sync=true)——同一个 key 未命中时,只让一个线程执行方法加载、其他线程等待并共享结果,避免大量并发同时查库;本质是加载加锁。

八、加强记忆

Spring Cache 是一套「缓存抽象」——用 @Cacheable/@CachePut/@CacheEvict 注解声明式给方法加缓存不关心底层是 Redis/Caffeine/EhCache(类比 JDBC:它是统一 API,具体缓存是「驱动」)。它定义两个核心接口:Cache(一块命名缓存,get/put/evict)和 CacheManager(管多个 Cache,getCache(name) 按名取,@Cacheable(value="users") 的 “users” 就是 Cache 名);不实现存储,由缓存提供者提供 RedisCacheManager/CaffeineCacheManager 等实现。工作原理是 AOP——@Cacheable 织入环绕通知:命中缓存直接返回、不执行原方法,未命中才执行并缓存;所以同类内部自调用会失效(和 @Transactional/@Async 同类问题)。三注解@Cacheable(读,命中跳过方法)、@CachePut(写,总执行方法并更新缓存)、@CacheEvict(删缓存);更新数据时用 @CachePut 同步或 @CacheEvict 删除(后者更常用)。key/condition/unless 用 SpELkey="#id" 引用参数,别写成字面量 "id"condition 方法前判断、unless 方法后判断能用 #resultsync=true 防缓存击穿)。换缓存实现只改依赖和配置、注解不动——这就是抽象的价值。一句话「Spring Cache 是缓存抽象(注解+Cache/CacheManager,不实现存储由 Redis/Caffeine 实现),靠 AOP 环绕通知(命中不执行方法、同类自调用失效);@Cacheable 读/@CachePut 写总执行/@CacheEvict 删,key 用#参数别写字面量;换实现只改配置注解不动」。