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.namecondition:满足条件才缓存(方法执行前判断)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+):
yamlspring: 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>
yamlspring: 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):
javapublic 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 | 分布式生产环境 | 支持集群和持久化 | 网络开销、需要运维 |