缓存抽象与 Redis 集成
约 1313 字大约 4 分钟
布欧-Lewyon
2026-05-15
首页 › Spring Boot › 缓存 › 缓存抽象与 Redis 集成
Spring Cache 抽象主张"声明式缓存"——通过在方法上加注解即可缓存返回值,无需直接操作 Jedis 或 RedisTemplate。
启用缓存
@SpringBootApplication
@EnableCaching // 开启缓存注解支持
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}@Cacheable
方法执行前先检查缓存,若命中则直接返回缓存值,不执行方法体:
@Service
public class UserService {
@Cacheable(value = "users")
public User findById(Long id) {
// 模拟慢查询
slowQuery();
return userRepository.findById(id).orElseThrow();
}
}首次调用 findById(1L) 会执行方法并缓存结果;再次调用相同参数时直接返回缓存。
条件缓存
// 仅当参数 id > 1000 时才缓存
@Cacheable(value = "users", condition = "#id > 1000")
// 除非 id = -1,否则缓存
@Cacheable(value = "users", unless = "#result == null")自定义缓存键
@Cacheable(value = "users", key = "#id + '-' + #locale")
public User findById(Long id, String locale) { ... }默认键基于所有参数生成,可通过 key 自定义 SpEL 表达式。
@CachePut
每次都执行方法,并更新缓存。适合更新场景:
@CachePut(value = "users", key = "#user.id")
public User update(User user) {
return userRepository.save(user);
}@CacheEvict
清除缓存:
// 清除指定键
@CacheEvict(value = "users", key = "#id")
public void deleteById(Long id) { ... }
// 清除整个 users 缓存
@CacheEvict(value = "users", allEntries = true)
public void deleteAll() { ... }@Caching 组合
多条缓存操作组合:
@Caching(
put = { @CachePut(value = "users", key = "#user.id") },
evict = { @CacheEvict(value = "userList", allEntries = true) }
)
public User save(User user) { ... }缓存后端切换
Caffeine(本地缓存,推荐)
<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>spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=500,expireAfterWrite=10mRedis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>spring:
cache:
type: redis
data:
redis:
host: localhost
port: 6379默认使用 JdkSerializationRedisSerializer,建议改为 JSON 提升可读性:
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}源码与架构:CacheInterceptor 执行流程
Spring Cache 的声明式缓存通过 AOP 实现,核心是 CacheInterceptor(继承 CacheAspectSupport)。理解其内部流程有助于处理缓存一致性、条件缓存等高级场景。
CacheInterceptor.invoke(invocation)
│
└── CacheAspectSupport.execute(cacheOperationInvoker, cacheOperationMetadata, ...)
│
├── ① 解析缓存元数据
│ └── CacheOperationSource.getCacheOperations(method, targetClass)
│ └── 获取 @Cacheable / @CachePut / @CacheEvict 注解元数据
│
├── ② 确定 Cache 对象
│ └── CacheResolver.resolveCaches(cacheOperation, ...)
│ └── CacheManager.getCache(name) → 按缓存名获取 Cache
│ ├── ConcurrentMapCache (SimpleCacheManager)
│ ├── CaffeineCache (CaffeineCacheManager)
│ ├── RedisCache (RedisCacheManager)
│ └── ...
│
├── ③ 生成缓存键
│ └── KeyGenerator.generate(target, method, params)
│ ├── SimpleKeyGenerator → 基于所有参数
│ └── SpEL 自定义 key → 在方法调用前解析
│
├── ④ 检查缓存命中(仅 @Cacheable)
│ ├── cache.get(key) → 命中则直接返回
│ └── 未命中(或 condition 不满足)→ 继续执行
│
├── ⑤ 调用目标方法(invokeOperation)
│ └── CacheOperationInvoker.invoke()
│
├── ⑥ 根据注解类型后处理
│ ├── @Cacheable: cache.put(key, result) ← 写入缓存
│ ├── @CachePut: cache.put(key, result) ← 总是写入
│ └── @CacheEvict: cache.evict(key) / cache.clear() ← 清理
│
└── ⑦ 返回结果缓存穿透、击穿与雪崩
| 问题 | 场景 | Spring Cache 应对 | 额外手段 |
|---|---|---|---|
| 穿透 | 查询不存在的数据(如 id=-1),每次都穿透到数据库 | @Cacheable(unless = "#result == null") | Bloom Filter 预过滤 |
| 击穿 | 热点 key 过期瞬间大量并发请求涌入 | 无内置锁(多线程同时调用目标方法) | Caffeine refreshAfterWrite;布隆过滤器;本地互斥锁 |
| 雪崩 | 大量 key 同时过期,数据库压力突增 | 无内置机制 | 设置随机过期时间;二级缓存(本地 + 远端) |
生产意识:
@Cacheable的unless表达式在方法执行后评估(结果可用),condition在方法执行前评估(结果不可用)。@Cacheable的同步模式(sync = true)可以解决击穿问题——它会使用目标方法的同步锁(基于Cache键的ReentrantLock),仅允许一个线程执行方法体,其他线程等待。但sync不能与unless同时使用,且仅支持SimpleKey生成器。如需更精细的控制(如分布式环境击穿保护),建议在业务层使用 Redis SETNX 实现互斥缓存重建。
@EnableCaching开启缓存注解,@Cacheable/@CachePut/@CacheEvict声明缓存行为。- Spring Cache 抽象支持切换后端:Caffeine(本地) → Redis(分布式)。
- 缓存键默认基于所有参数,
key可自定义 SpEL 表达式。 - 易错:
@Cacheable默认缓存方法返回值,如果方法抛出异常不会缓存。缓存穿透(查询不存在的数据反复击穿数据库)可用unless = "#result == null"避免缓存 null。缓存序列化方式不当时(如默认 JDK 序列化),可能因ClassNotFoundException导致反序列化失败。 - 生产意识:缓存击穿(缓存过期瞬间大量并发请求穿透到数据库)是线上高并发服务常遇到的故障。
@Cacheable并不能阻止击穿——多个线程同时发现缓存过期后都会执行方法体。解决方案:①使用 Caffeine 的expireAfterWrite配合refreshAfterWrite,在过期前异步刷新缓存;②使用 Redis 的分布式锁做缓存重建互斥。缓存的 key 设计也很关键——key 过长(如拼接整个 DTO)不仅浪费内存,还可能导致 Redis 内存碎片。推荐使用业务语义明确的前缀 + 唯一标识(如user:profile:1001)。 - 思考任务:在 Service 层的查询方法上添加
@Cacheable,使用 Caffeine 作为缓存后端,验证第二次相同参数调用不执行方法体。
上一节:数据库迁移
下一节:任务管理
