缓存是提升系统吞吐量最直接的手段之一,而Redis凭借内存存储和丰富的数据结构,几乎成了Java后端缓存的默认选择。Spring框架从3.1开始提供了统一的缓存抽象层,也就是Spring Cache,它把缓存的读写逻辑从业务代码中剥离出来,开发者只需要在方法上加几个注解,就能完成缓存的查询、更新和清除。这篇文章将围绕Redis与Spring Cache的整合展开,从基本用法讲到配置细节,再到常见的踩坑点,帮你把缓存这套机制真正用明白。

一、Spring Cache抽象的核心概念与注解
Spring Cache的设计思路与Spring对事务的管理类似,都是基于AOP代理实现的。当Spring容器启动时,会对标注了缓存注解的Bean生成代理对象,方法调用先经过缓存拦截器,命中缓存则直接返回,未命中才执行真实方法并把结果写入缓存。这个机制决定了它对业务代码几乎零侵入,你不需要在Service里手写RedisTemplate的get和set逻辑。
核心注解主要有三个:@Cacheable负责查询缓存,方法执行前先查,命中则跳过方法体;@CachePut每次都会执行方法并把返回值刷新到缓存,适合更新场景;@CacheEvict用于删除缓存,支持按key删除和全量清空。此外还有@Caching用于组合多个注解,以及@CacheConfig用于在类级别统一声明缓存名称。看一个典型例子:
@Service
public class UserService {
// 先查user缓存,key为参数id,未命中则执行方法并缓存结果
@Cacheable(value = "user", key = "#id")
public User getUserById(Long id) {
System.out.println("从数据库查询用户: " + id);
return userMapper.selectById(id);
}
// 更新用户并刷新缓存
@CachePut(value = "user", key = "#user.id")
public User updateUser(User user) {
userMapper.updateById(user);
return user;
}
// 删除用户时清除对应缓存
@CacheEvict(value = "user", key = "#id")
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
}key属性支持SpEL表达式,这是Spring Cache最灵活的地方。#id表示方法参数,#result表示返回值(仅在@CachePut和@CacheEvict的beforeInvocation为false时可用),#p0、#a0这类下标写法也支持。如果方法有多个参数,可以拼接成组合键,例如key = "#userId + ':' + #orderId"。当不指定key时,Spring会使用内置的SimpleKeyGenerator,把所有参数拼成一个key,无参方法则用SimpleKey.EMPTY表示。需要注意的是,无参方法加@Cacheable意味着所有调用共享同一个缓存值,这在实际业务中往往是错误设计,务必想清楚缓存粒度。
二、整合Redis:依赖引入与缓存管理器配置
要让Spring Cache的底层存储变成Redis,需要引入spring-boot-starter-data-redis和spring-boot-starter-cache两个依赖,然后在配置类上添加@EnableCaching开启缓存支持。此时Spring Boot会自动装配RedisCacheManager,缓存的key会被加上前缀(默认是缓存名加双冒号),value则默认使用JDK序列化。JDK序列化虽然能用,但生成的字节数组体积大、可读性差,还要求实体类实现Serializable接口,生产环境一般都会换成JSON序列化。
自定义RedisCacheManager的核心在于序列化器和TTL的设置。下面是一份常用配置:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 缓存key前缀,entryTtl设置默认过期时间30分钟
.prefixCacheNameWith("app:")
.entryTtl(Duration.ofMinutes(30))
// 禁用缓存空值(也可选择开启以防缓存穿透)
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 针对不同缓存名设置差异化的过期时间
Map<String, RedisCacheConfiguration> configs = new HashMap<>();
configs.put("user", config.entryTtl(Duration.ofHours(2)));
configs.put("hotData", config.entryTtl(Duration.ofMinutes(5)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(configs)
.build();
}
}这份配置里有几个值得展开的点。第一,prefixCacheNameWith给所有key加了统一前缀,多项目共用一个Redis实例时可以避免key冲突。第二,entryTtl支持按缓存名分别设置TTL,热点数据可以用较短的过期时间保证新鲜度,相对稳定的数据则设置长一些减少回源。第三,GenericJackson2JsonRedisSerializer会在JSON中写入类型信息,反序列化时能还原成原始对象,这是它比普通的Jackson2JsonRedisSerializer(需要每个类型单独构造)更省事的地方,代价是JSON体积稍大。如果你的实体类结构频繁变动,类型信息还可能导致反序列化失败,这时要考虑版本兼容问题。
另外提醒一点,使用JSON序列化时如果实体的某些字段是LocalDateTime等Java 8时间类型,需要ObjectMapper注册JavaTimeModule,否则序列化会直接抛异常。这个坑非常常见,报错信息往往只是一句SerializationException,排查时别忘了检查时间字段。
三、注解失效的常见陷阱与最佳实践
Spring Cache基于AOP代理,这就带来了一个经典问题:类内部方法自调用会导致注解失效。比如同一个Service里有方法A调用标注了@Cacheable的方法B,调用A时走的是this引用而非代理对象,缓存拦截根本不会发生。解决办法通常有三种:把方法拆到另一个Bean中;注入自身代理(通过AopContext.currentProxy()并开启exposeProxy);或者干脆不依赖注解,改用RedisTemplate手动控制。实践中推荐第一种,结构也更清晰。
还有几类高频问题需要留意。其一是缓存的key设计不当导致读到脏数据,比如对象更新后缓存key没变但内容变了,务必保证key与业务唯一标识严格对应。其二是缓存穿透,查询一个数据库中不存在的id,每次都会穿透到数据库,可以通过.computeIfAbsent之外的方案,也就是开启空值缓存(去掉disableCachingNullValues并用unless = "#result == null"控制)或者引入布隆过滤器解决。其三是缓存雪崩,大量key同时过期会造成数据库瞬时压力,除了给TTL加随机偏移量,还可以结合多级缓存思路,本地Caffeine加远程Redis,短TTL放本地、长TTL放Redis。
对比一下Spring Cache注解方式和直接使用RedisTemplate的适用场景:注解方式适合读写模式固定、以整个方法返回值为缓存单元的场景,代码简洁、维护成本低;RedisTemplate适合需要精细操作Redis数据结构(比如Hash的局部更新、计数器、分布式锁)的场景,灵活但代码量大。两者并不互斥,实际项目中通常是主体查询走注解缓存,特殊逻辑手动操作,各自发挥优势。
最后总结一下:Spring Cache把缓存从业务代码中解耦出来,配合Redis可以低成本获得性能提升,但它的便利建立在理解其代理机制和key策略的基础上。配置好序列化器、设计好TTL、避开自调用陷阱,再针对穿透和雪崩做好防御,这套缓存体系就能稳定支撑高并发场景。如果你的项目还在用散落各处的RedisTemplate硬编码,不妨逐步迁移到注解缓存,代码的可读性和可维护性会有明显改善。
RedisSpring CacheJava缓存修改时间:2026-08-31 22:49:01