1、概述
在 Spring Boot 中做缓存,最佳实践是使用 Spring Cache 抽象。它就像一套"统一的 API 开关",让你能用几乎相同的代码操作本地缓存(Caffeine)或分布式缓存(Redis),切换时只需改配置,业务代码无需改动。
1.1 两种方案
- 本地缓存 Caffeine:进程内存,速度极快,无网络开销;多实例下缓存不互通,数据不一致风险;适合静态、变化少的热点数据。
- Redis 缓存:分布式共享缓存;有网络 IO;多实例数据统一;适合分布式项目。
Spring Cache 抽象:一套注解,底层切换 Caffeine/Redis,业务代码几乎不用改动。
1.2 Spring Cache 核心注解
| 注解 | 作用 |
|---|---|
@Cacheable |
查询:先读缓存,命中直接返回;未命中执行方法,结果写入缓存 |
@CachePut |
更新:执行方法,把返回值更新缓存,不会拦截方法 |
@CacheEvict |
删除:清除指定缓存 key |
@Caching |
组合多个缓存操作 |
- key:缓存键;cacheNames:缓存分组;condition:条件缓存;unless:条件不缓存。
2、方案 1:本地缓存 Caffeine
2.1 引入依赖
<!-- spring cache 基础 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine本地缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
2.2 开启缓存
启动类增加 @EnableCaching
@SpringBootApplication
@EnableCaching
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
2.3 YAML 配置 Caffeine
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=300s,recordStats
参数说明:
maximumSize=1000:缓存最大条目,超过 LRU 淘汰expireAfterWrite=300s:写入后 300 秒过期recordStats:开启缓存统计命中率
2.4 Controller/ Service 使用示例
// cacheNames缓存分组;key支持SpEL表达式;condition:id>0才缓存
@Cacheable(cacheNames = "userInfo", key = "#id", condition = "#id>0")
public UserDTO getUserById(Long id){
// 未命中缓存执行DB查询逻辑
return userMapper.selectById(id);
}
// 更新数据同步更新缓存
@CachePut(cacheNames = "userInfo", key="#user.id")
public UserDTO updateUser(User user){
userMapper.updateById(user);
return convert(user);
}
// 删除数据清除缓存
@CacheEvict(cacheNames = "userInfo", key="#id")
public void deleteUser(Long id){
userMapper.deleteById(id);
}
注意:
@Cacheable基于 AOP,同类内部调用不会生效;不要 this 调用本类带缓存方法。- 多实例部署,Caffeine 每个实例缓存独立,写操作后其他节点缓存不会自动失效。
3、方案 2:Redis 分布式缓存
3.1 Maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.2 application.yml 配置
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
cache:
type: redis
redis:
time-to-live: 300s #全局默认TTL
cache-null-values: false #是否缓存null,防止缓存穿透
3.3 自定义 Redis 序列化(重要,默认 JDK 序列化,二进制可读性差)
需要@EnableCaching。
package com.ybw.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* Redis 缓存统一配置
* <p>
* 同时管理两套 Redis 访问通道,且序列化策略保持一致,保证数据互通:
* <ul>
* <li>{@link RedisCacheManager} ------ 服务于 @Cacheable / @CachePut / @CacheEvict 注解</li>
* <li>{@link RedisTemplate} ------ 服务于手动编码 opsForValue / opsForHash 等操作</li>
* </ul>
* 两者均使用 GenericJackson2JsonRedisSerializer(@class 属性携带类型信息),
* 因此注解写入的数据可被 redisTemplate 读取,反之亦然。
*/
@Configuration
@EnableCaching
public class CacheConfig {
/**
* 全局默认 TTL。
* <p>
* 注意:由于本类自定义了 RedisCacheManager Bean,
* application.yml 中 spring.cache.redis.time-to-live 不会被 Spring 自动装配,
* 必须通过 @Value 手动读取后注入到 RedisCacheConfiguration 中。
*/
@Value("${spring.cache.redis.time-to-live:30s}")
private Duration timeToLive;
// ==================== Spring Cache 注解体系 ====================
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
//1、RedisCacheConfiguration 配置,只对@cache相关注解生效
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(timeToLive)
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer(buildObjectMapper())))
.disableCachingNullValues();
//2、RedisCacheManager 配置,对@cache相关注解生效
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfig)
.build();
}
// ==================== 手动操作 Redis 体系 ====================
/**
* 配置 RedisTemplate,key 使用 String 序列化,value 使用 Jackson JSON 序列化。
* <p>
* 序列化器与 cacheManager 保持一致(GenericJackson2JsonRedisSerializer + @class 类型信息),
* 确保两套通道写入的数据可互相读取。
*
* @param factory Redis 连接工厂
* @return 配置好的 RedisTemplate,key 为 String,value 为任意 Object
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer(buildObjectMapper());
// key / hashKey 一律用 String 序列化,保证 Redis 中 key 可读、可命令行操作
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// value / hashValue 用 Jackson JSON 序列化,携带 @class 类型信息
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
// ==================== 公共工具方法 ====================
/**
* 构建统一的 Jackson ObjectMapper,消除重复配置。
* <p>
* 统一要点:
* <ul>
* <li>activateDefaultTyping:JSON 中以 @class 属性携带运行时类型,
* 保证 User / List<User> / Map 等可正确反序列化</li>
* <li>JavaTimeModule:支持 LocalDateTime / LocalDate 等 JSR-310 类型</li>
* <li>关闭 WRITE_DATES_AS_TIMESTAMPS:日期序列化为 ISO-8601 字符串而非时间戳数组</li>
* </ul>
*
* @return 配置好的 ObjectMapper
*/
private ObjectMapper buildObjectMapper() {
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
om.registerModule(new JavaTimeModule());
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return om;
}
}
4、方案 3:多级缓存(Caffeine 本地 + Redis 二级缓存,高性能)
- 流程:请求 → Caffeine 本地缓存命中直接返回 → 未命中查询 Redis → Redis 命中返回写入 Caffeine → Redis 未命中查询 DB。
- Spring Cache 原生不支持多级缓存,需要自己封装,不能直接注解完成。
多级缓存注意点
- 写操作时:删除 Redis,同时删除本机 Caffeine;其他机器本地缓存依旧残留脏数据。
- 解决手段:Redis 发布订阅,更新缓存后广播消息,所有实例删除本地缓存。
5、使用选型建议
| 场景 | 缓存选型 |
|---|---|
| 单实例部署、热点不变基础数据 | Caffeine 本地缓存 |
| 分布式集群、多实例共享数据 | Redis 缓存 |
| 高 QPS 接口,追求极致性能 | Caffeine+Redis 多级缓存 |