5月28日 01:31

Spring Boot 中如何实现缓存?

核心回答

Spring Boot 通过 Spring Cache Abstraction 提供统一的缓存抽象,开发者只需添加 @EnableCaching 注解和对应缓存实现依赖,即可用 @Cacheable@CachePut@CacheEvict 等注解实现声明式缓存。常用实现方案有三种:

  • ConcurrentMapCache:基于 ConcurrentHashMap,零依赖,适合单机开发测试
  • Caffeine:高性能本地缓存,支持过期策略和容量限制,适合单机生产环境
  • Redis:分布式缓存,支持持久化和集群,适合多实例部署

选择依据:单机选 Caffeine,分布式选 Redis,开发调试用 ConcurrentMapCache。

缓存注解用法

@Cacheable —— 查询时缓存

方法执行前先查缓存,命中则直接返回,未命中才执行方法并缓存结果:

java
@Cacheable(value = "users", key = "#id", unless = "#result == null") public User getUserById(Long id) { return userRepository.findById(id).orElse(null); }
  • key:支持 SpEL 表达式,如 #id#user.name
  • condition:满足条件才缓存(方法执行前判断)
  • unless:满足条件则不缓存(方法执行后判断)

@CachePut —— 更新缓存

方法一定执行,执行后用返回值更新缓存:

java
@CachePut(value = "users", key = "#user.id") public User updateUser(User user) { return userRepository.save(user); }

@CacheEvict —— 删除缓存

java
// 删除指定 key @CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { ... } // 清空整个缓存区域 @CacheEvict(value = "users", allEntries = true) public void clearUserCache() { }

beforeInvocation = true 可在方法执行前删缓存,防止方法异常导致缓存未清除。

@Caching —— 组合操作

java
@Caching( put = { @CachePut(value = "users", key = "#user.id") }, evict = { @CacheEvict(value = "userList", allEntries = true) } ) public User saveUser(User user) { return userRepository.save(user); }

Caffeine 本地缓存配置

xml
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
java
@Configuration @EnableCaching public class CaffeineCacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .initialCapacity(100) .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .recordStats()); return manager; } }

YAML 简写方式(Spring Boot 2.7+):

yaml
spring: cache: type: caffeine caffeine: spec: maximumSize=1000,expireAfterWrite=10m

不同缓存区域可用 registerCustomCache 分别配置过期时间和容量。

Redis 分布式缓存配置

xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
yaml
spring: redis: host: localhost port: 6379 cache: type: redis redis: time-to-live: 600000 cache-null-values: false key-prefix: "myapp:"
java
@Configuration @EnableCaching public class RedisCacheConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); Map<String, RedisCacheConfiguration> configs = new HashMap<>(); configs.put("users", defaults.entryTtl(Duration.ofMinutes(30))); configs.put("products", defaults.entryTtl(Duration.ofMinutes(5))); return RedisCacheManager.builder(factory) .cacheDefaults(defaults) .withInitialCacheConfigurations(configs) .transactionAware() .build(); } }

缓存穿透、击穿、雪崩

问题原因解法
穿透查询不存在的数据,缓存和DB都没有缓存空值(短TTL)或布隆过滤器
击穿热点key过期,大量请求同时打到DB互斥锁或逻辑过期
雪崩大量key同时过期随机过期时间 + 多级缓存

互斥锁示例(基于 Redisson):

java
public User getUserWithLock(Long id) { String key = "users::" + id; String cached = redisTemplate.opsForValue().get(key); if (cached != null) return JSON.parseObject(cached, User.class); RLock lock = redissonClient.getLock("lock:users:" + id); try { if (lock.tryLock(10, 30, TimeUnit.SECONDS)) { try { cached = redisTemplate.opsForValue().get(key); if (cached != null) return JSON.parseObject(cached, User.class); User user = userRepository.findById(id).orElse(null); if (user != null) { redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES); } return user; } finally { lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return null; }

面试追问

@Cacheable 和 @CachePut 有什么区别? @Cacheable 会先查缓存,命中则跳过方法执行;@CachePut 不查缓存,方法一定执行后用返回值更新缓存。更新场景必须用 @CachePut,否则缓存不会刷新。

缓存和事务一起用要注意什么? Spring 缓存注解基于 AOP 代理,在事务边界之外执行。如果事务回滚,缓存可能已经写入脏数据。建议写操作用 @CachePut 且在事务提交后再更新,或用 TransactionSynchronizationManager.registerSynchronization 在事务提交后操作缓存。

多级缓存怎么实现? L1 用 Caffeine(本地,毫秒级),L2 用 Redis(分布式,5-10ms)。读取时先查 L1,未命中查 L2,再未命中查 DB 并回写 L1 和 L2。更新时先更新 DB,再删 L1 和 L2 缓存。可用 CompositeCacheManager 组合多个 CacheManager。

Spring Cache 的 key 生成规则是什么? 默认用 SimpleKeyGenerator:无参用 SimpleKey.EMPTY,一个参数直接用该参数,多个参数用 SimpleKey(params)。自定义可实现 KeyGenerator 接口,通过 keyGenerator 属性引用。

方案选型总结

缓存类型适用场景优点缺点
ConcurrentMapCache单机开发测试零配置、无额外依赖不支持过期和分布式
Caffeine单机生产环境高性能、支持过期淘汰不支持多实例共享
Redis分布式生产环境支持集群和持久化网络开销、需要运维
标签:Spring Boot