Spring 的缓存抽象并不负责具体缓存存储,而是提供一层统一的注解驱动模型。借助 AOP,业务方法在调用前后被拦截,缓存读写由代理完成。Spring Boot 自动配置进一步简化了接入步骤,只需引入对应缓存实现,配置连接信息,就能在方法上通过 @Cacheable 等注解声明缓存行为。

一、缓存注解与代理执行模型
Spring Cache 的核心注解包括 @Cacheable、@CacheEvict、@CachePut 和 @Caching。@Cacheable 用于查询方法,方法被执行前会先检查缓存,命中则直接返回缓存值,未命中才执行方法体并将结果写入缓存。@CacheEvict 负责失效缓存,通常用在更新或删除操作上。@CachePut 与 @Cacheable 相反,它总会执行方法,并把返回结果更新到缓存中,适合需要刷新缓存但又不希望跳过方法逻辑的场景。
这些注解之所以能生效,是因为 Spring 在容器初始化阶段为标注了缓存注解的 Bean 创建了代理对象。实际调用进入代理后,由 CacheInterceptor 统一解析注解元数据,再交给 CacheAspectSupport 执行缓存读写。整个链路与事务管理类似,开发者只需要关注注解配置是否正确,底层对缓存 API 的调用细节全部被屏蔽。下面是一个最基础的查询缓存示例:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Cacheable(value = "user", key = "#id")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
这里的 value 指定缓存名称,key 使用 SpEL 表达式取方法入参 id。首次调用 getUserById(1L) 会执行数据库查询并把结果放入名为 user 的缓存区域;再次使用相同参数调用时,方法体不会执行,直接从缓存返回对象。
二、Spring Boot 配置 Redis Cache
如果只使用本地简单缓存,Spring Boot 默认会基于 ConcurrentHashMap 创建 ConcurrentMapCacheManager。这种方式适合单机测试,但生产环境通常需要分布式缓存。Redis 是 Spring Cache 最常用的实现之一,接入后缓存数据可以跨实例共享,并且支持过期时间、序列化策略等更细粒度的控制。
在 Spring Boot 项目中,只需在 pom.xml 或 Gradle 构建脚本中加入 spring-boot-starter-data-redis 和 spring-boot-starter-cache 依赖。然后通过 application.yml 指定缓存类型为 Redis:
spring:
cache:
type: redis
data:
redis:
host: localhost
port: 6379
password:
database: 0
仅仅这样配置还不够,默认的 JDK 序列化方式要求缓存对象实现 Serializable 接口,生成的数据可读性差,占用空间也较大。实际项目更推荐使用 JSON 序列化方案。下面配置 RedisCacheManager 使用 GenericJackson2JsonRedisSerializer,并为不同缓存名称设置不同的过期时间:
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("user", defaultConfig.entryTtl(Duration.ofHours(2)));
cacheConfigs.put("product", defaultConfig.entryTtl(Duration.ofMinutes(10)));
return RedisCacheManager.builder(factory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
}
这段配置里,entryTtl 控制全局默认过期时间,withInitialCacheConfigurations 可以为不同命名空间单独设置 TTL。StringRedisSerializer 让键以可读字符串形式存储,GenericJackson2JsonRedisSerializer 把值序列化为 JSON,便于用 Redis 客户端直接查看和调试。需要注意 JSON 序列化会保留类型信息,反序列化时依赖类路径一致。
三、自定义 Key 与条件缓存策略
缓存 Key 的生成方式直接影响命中率。默认情况下,Spring 使用 SimpleKeyGenerator,当方法只有一个参数时,直接以该参数作为 Key;多个参数时则组合成 SimpleKey 对象。这种默认规则在参数顺序或对象状态变化时可能导致缓存错位,因此显式指定 SpEL 表达式是更稳妥的做法。
下面的示例展示了组合 Key 和条件缓存的使用:
@Cacheable(
value = "product",
key = "#category + ':' + #page + ':' + #size",
condition = "#page >= 0 && #size > 0",
unless = "#result == null"
)
public List<Product> listProducts(String category, int page, int size) {
return productRepository.findByCategory(category, page, size);
}
这里 condition 在方法执行前判断,只有满足条件才走缓存逻辑;unless 在方法执行后判断,如果表达式成立,结果不会被写入缓存。例子中 page >= 0 和 size > 0 是条件缓存,返回值为 null 时不缓存,可以防止无效数据占据 Redis 空间。
如果 SpEL 表达式无法满足复杂场景,可以实现 KeyGenerator 接口,通过方法名、目标对象和入参自定义 Key 拼接规则。例如:
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName() + ":" + Arrays.toString(params);
}
}
在注解中通过 keyGenerator = "customKeyGenerator" 引用即可。自定义 KeyGenerator 适合需要统一加版本号或租户标识的项目,避免每个注解都写一遍 SpEL 表达式。
四、缓存更新、失效与一致性防护
缓存读多写少的场景中,最难处理的是数据一致性。更新数据库后如果不及时清理缓存,后续请求可能拿到旧数据。Spring Cache 提供了 @CacheEvict 来处理失效操作。常见做法是在更新方法成功后删除对应缓存:
@CacheEvict(value = "user", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
如果更新方法会同时影响多个缓存区域,可以使用 @Caching 组合多个失效规则。allEntries = true 可以清空整个命名空间,适合全量刷新场景:
@Caching(
evict = {
@CacheEvict(value = "user", key = "#user.id"),
@CacheEvict(value = "userList", allEntries = true)
}
)
public User updateUserProfile(User user) {
return userRepository.save(user);
}
需要特别注意的是,@CacheEvict 默认在方法成功返回后执行。如果方法抛出异常,缓存不会被清理,这有利于事务一致性。但某些场景希望无论业务方法是否成功都清理缓存,可以设置 beforeInvocation = true。不过这种策略在事务回滚时可能导致缓存被提前删除,因此要结合具体业务谨慎使用。
在高并发访问下,缓存穿透和击穿是常见风险。缓存穿透指大量请求查询一个不存在的数据,缓存层无法命中,数据库压力骤增。可以在业务层对空结果设置短 TTL 占位缓存,或者使用布隆过滤器拦截非法 Key。缓存击穿则是某个热点 Key 在过期瞬间收到大量并发请求,同时回源数据库。可以通过互斥锁、逻辑过期或 sync = true 让 @Cacheable 在回源时加锁,降低击穿概率。Spring Cache 本身不提供完整的多级缓存方案,但可以在 CacheErrorHandler 中记录缓存操作异常,避免缓存中间件故障影响主流程。
总体来说,Spring Boot 整合 Spring Cache 的核心理念是注解声明加配置驱动。对查询方法用 @Cacheable,对更新方法用 @CachePut 或 @CacheEvict,再通过 Redis 或本地缓存实现具体存储。Key 策略和序列化方案需要根据数据规模、并发量以及团队维护习惯提前规划,否则后期调整成本会比较高。
Spring BootSpring Cache缓存抽象修改时间:2026-09-24 18:42:23