在业务量上来之后,数据库往往最先扛不住压力,此时加一层缓存几乎是标配方案。Spring Boot 提供的 Spring Cache 抽象层让缓存操作变得非常轻量,方法级别的一个注解就能把返回结果缓存起来,下次同样的请求直接从缓存读取,不必再查询数据库。本文将从原理讲到实操,完整演示如何整合默认缓存与 Redis 缓存,并覆盖常见的坑点。

一、Spring Cache 的核心原理与常用注解
Spring Cache 本身不提供具体的缓存实现,它只是一层抽象,背后可以对接 ConcurrentMap、Ehcache、Redis、Caffeine 等不同存储。启用缓存的入口是在配置类或启动类上标注 @EnableCaching,Spring 会扫描带有缓存注解的 Bean,为它们生成代理对象,在方法调用前后插入缓存读写逻辑。
整个过程可以简单理解为:方法被调用时,先根据缓存的 key 去缓存中查找,命中则直接返回缓存值并跳过方法体;未命中则执行方法,把返回值写入缓存,再返回给调用者。这套机制对业务代码零侵入,只需要在方法上加注解。
常用注解有五个,职责各不相同:
@Cacheable:先查缓存,命中直接返回,未命中执行方法并写入缓存,最常用于查询方法。@CachePut:每次都执行方法,并用返回值更新缓存,适合写操作后刷新缓存。@CacheEvict:删除缓存,可通过allEntries = true清空整个缓存空间。@Caching:组合多个缓存操作,用于一个方法需要同时增删多个缓存的场景。@CacheConfig:类级别的公共配置,统一指定缓存名称和 key 生成器,避免每个方法重复写。
二、快速接入默认缓存与 Redis 缓存
1. 启用缓存并使用内存缓存
第一步是引入依赖并开启缓存。如果不加任何缓存组件,Spring Boot 默认使用 ConcurrentMapCacheManager,数据存在内存里,适合单体小应用和本地调试。
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Service
public class UserService {
// value 指定缓存空间,key 指定缓存的键
@Cacheable(value = "user", key = "#id")
public User getUserById(Long id) {
// 模拟一次耗时的数据库查询
User user = userMapper.selectById(id);
System.out.println("从数据库查询:" + id);
return user;
}
// 更新数据的同时刷新缓存
@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);
}
}调用 getUserById 两次,控制台只会打印一次数据库查询日志,说明第二次直接命中了缓存。这里要注意 key 的写法,#id 引用的是方法参数,如果参数是对象则用 #user.id 访问属性。
2. 切换到 Redis 作为缓存存储
内存缓存在应用重启后会丢失,多实例部署时各节点缓存也不互通,生产环境一般换成 Redis。引入依赖后,Spring Boot 会自动装配 RedisCacheManager,无需额外配置类即可生效。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>spring:
redis:
host: 127.0.0.1
port: 6379
password: 你的密码
database: 0
timeout: 5000换上 Redis 之后,业务代码完全不用动,注解还是原来那些,这就是缓存抽象层的价值所在。此时用 Redis 客户端连接上去,能看到 key 的格式类似 user::1,前面是缓存空间名,后面是具体的 key。
三、生产环境必备的进阶配置
1. 自定义序列化与过期时间
默认的 Redis 序列化用的是 JDK 序列化,存进去的值是乱码二进制,可读性差且要求实体类实现序列化接口。推荐改成 JSON 格式,同时为不同缓存空间设置不同的过期时间,比如热点数据时间短一些,字典类数据时间长一些。
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 设置 value 为 JSON 序列化
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
// key 使用字符串序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
// 禁止缓存 null 值
.disableCachingNullValues()
// 默认过期时间 30 分钟
.entryTtl(Duration.ofMinutes(30));
// 为不同缓存空间单独设置过期时间
Map<String, RedisCacheConfiguration> configs = new HashMap<>();
configs.put("user", config.entryTtl(Duration.ofMinutes(10)));
configs.put("dict", config.entryTtl(Duration.ofHours(6)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(configs)
.build();
}
}JSON 序列化还有一个隐藏问题:GenericJackson2JsonRedisSerializer 会把对象的类型信息一起存进去,反序列化时能还原为原始类型。但如果缓存的是没有类型的集合或接口对象,可能会报类型转换异常,需要在实体类上做好泛型约定,或者改用带类型信息的序列化器。
2. 缓存穿透与空值处理
如果某个 id 在数据库中不存在,每次请求都不会命中缓存,请求全部打到数据库,这就是典型的缓存穿透。常见的解决办法是缓存空值:把查询结果为 null 的情况也缓存一小段时间,配合 unless 属性即可实现。
@Cacheable(value = "user", key = "#id",
unless = "#result == null") // 结果为 null 不缓存的写法要反过来
public User getUserById(Long id) {
return userMapper.selectById(id);
}
// 缓存空值的写法:返回一个空对象标记,并在配置中开启 null 缓存
@Cacheable(value = "user", key = "#id", unless = "#result?.id == null")
public User getUserSafe(Long id) {
return Optional.ofNullable(userMapper.selectById(id))
.orElse(new User()); // 空对象代替 null
}除了穿透问题,还要注意缓存雪崩:大量 key 在同一时间过期会导致数据库瞬时压力激增,解决思路是给过期时间加上随机偏移,比如基准 30 分钟加上 0 到 5 分钟的随机值,把过期时间点打散。
3. 使用 Caffeine 构建多级缓存
对于读多写少的场景,可以在 Redis 前面再加一层本地缓存,组成两级缓存结构。Spring Boot 对 Caffeine 的支持非常友好,引入 spring-boot-starter-cache 和 Caffeine 依赖后,在配置文件中直接声明即可。
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=5m本地缓存命中速度极快,但要考虑多实例之间的数据一致性问题。如果对一致性要求高,本地缓存的过期时间要设置得短一些,或者在数据变更时通过消息队列广播失效通知,主动清理各节点的本地缓存。
四、常见踩坑总结
第一,@Cacheable 方法必须通过代理调用才生效,也就是必须由外部调用。同类中 this 调用自己的缓存方法,注解不会生效,这是新手最容易遇到的问题。解决办法是把缓存方法拆到单独的 Service 中,或者通过注入自身代理对象来调用。
第二,缓存的 key 拼接要保证唯一性。多个参数时如果只写一个参数作为 key,可能造成不同请求命中同一条缓存。建议使用 SpEL 表达式拼接完整 key,例如 key = "#type + ':' + #id",或者自定义 KeyGenerator 统一管理。
第三,注意事务与缓存的执行顺序。缓存写入发生在方法返回之后,如果方法在事务中执行,可能出现缓存写入成功但事务回滚的情况,导致缓存与数据库不一致。高一致性要求的场景要把缓存操作放到事务提交之后处理,可以借助 TransactionSynchronization 实现。
掌握这些内容后,在 Spring Boot 项目中落地一套缓存体系就不再是难事。默认内存缓存适合快速验证,Redis 适合绝大多数生产场景,Caffeine 加 Redis 的两级缓存则能进一步提升高并发下的读取性能,按业务规模循序渐进地升级即可。
Spring Boot缓存Spring Cache注解Redis缓存整合修改时间:2026-09-04 20:26:42