Spring Boot 缓存详解
为什么需要缓存
- 减少数据库压力:热点数据直接从缓存读取
- 提升响应速度:内存访问比磁盘快几个数量级
- 降低系统负载:减少重复计算
- 改善用户体验:页面加载更快
Spring Boot 缓存抽象
Spring 提供了一套缓存抽象,支持多种缓存实现:
- ConcurrentMapCache:基于内存,单机使用
- Caffeine:高性能本地缓存
- EhCache:老牌缓存框架
- Redis:分布式缓存
- Hazelcast:分布式内存数据网格
实现方式一:基于内存的 ConcurrentMapCache
1. 开启缓存支持
java@Configuration @EnableCaching public class CacheConfig { /** * 配置缓存管理器 */ @Bean public CacheManager cacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager(); // 创建多个缓存区域 List<Cache> caches = new ArrayList<>(); caches.add(new ConcurrentMapCache("users")); caches.add(new ConcurrentMapCache("products")); caches.add(new ConcurrentMapCache("orders")); cacheManager.setCaches(caches); return cacheManager; } }
2. 使用缓存注解
java@Service @Slf4j public class UserService { @Autowired private UserRepository userRepository; /** * @Cacheable:先查缓存,没有则执行方法并缓存结果 * value:缓存名称 * key:缓存键,支持 SpEL * unless:条件判断,为 true 时不缓存 */ @Cacheable(value = "users", key = "#id", unless = "#result == null") public User getUserById(Long id) { log.info("从数据库查询用户: {}", id); return userRepository.findById(id).orElse(null); } /** * 使用多个 key 组合 */ @Cacheable(value = "users", key = "#username + ':' + #age") public User getUserByUsernameAndAge(String username, Integer age) { log.info("从数据库查询用户: {}, {}", username, age); return userRepository.findByUsernameAndAge(username, age); } /** * @CachePut:先执行方法,再更新缓存 * 适用于更新操作 */ @CachePut(value = "users", key = "#user.id") public User updateUser(User user) { log.info("更新用户: {}", user.getId()); return userRepository.save(user); } /** * @CacheEvict:删除缓存 * allEntries = true:清空整个缓存区域 * beforeInvocation = true:方法执行前删除缓存 */ @CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { log.info("删除用户: {}", id); userRepository.deleteById(id); } /** * 清空整个 users 缓存 */ @CacheEvict(value = "users", allEntries = true) public void clearUserCache() { log.info("清空用户缓存"); } /** * @Caching:组合多个缓存操作 */ @Caching( put = { @CachePut(value = "users", key = "#user.id"), @CachePut(value = "users", key = "#user.username") }, evict = { @CacheEvict(value = "users", key = "'list'") } ) public User saveUser(User user) { log.info("保存用户: {}", user.getId()); return userRepository.save(user); } }
实现方式二:Caffeine 高性能本地缓存
1. 添加依赖
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>
2. 配置 Caffeine
java@Configuration @EnableCaching public class CaffeineCacheConfig { /** * Caffeine 缓存配置 */ @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); // 配置默认的 Caffeine 规格 cacheManager.setCaffeine(Caffeine.newBuilder() // 初始容量 .initialCapacity(100) // 最大容量 .maximumSize(1000) // 写入后 10 分钟过期 .expireAfterWrite(10, TimeUnit.MINUTES) // 访问后 5 分钟过期 .expireAfterAccess(5, TimeUnit.MINUTES) // 记录缓存统计 .recordStats() ); // 指定缓存名称 cacheManager.setCacheNames(Arrays.asList("users", "products", "orders")); return cacheManager; } /** * 为不同缓存配置不同策略 */ @Bean("customCacheManager") public CacheManager customCacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); // 用户缓存:高频访问,长期有效 cacheManager.registerCustomCache("users", Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(30, TimeUnit.MINUTES) .recordStats() .build() ); // 产品缓存:中等频率,短期有效 cacheManager.registerCustomCache("products", Caffeine.newBuilder() .maximumSize(5000) .expireAfterWrite(10, TimeUnit.MINUTES) .build() ); // 订单缓存:低频访问,快速过期 cacheManager.registerCustomCache("orders", Caffeine.newBuilder() .maximumSize(1000) .expireAfterAccess(5, TimeUnit.MINUTES) .build() ); return cacheManager; } }
3. YAML 配置方式(Spring Boot 2.7+)
yamlspring: cache: type: caffeine caffeine: spec: maximumSize=1000,expireAfterWrite=10m
实现方式三:Redis 分布式缓存
1. 添加依赖
xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
2. 配置 Redis
yamlspring: redis: host: localhost port: 6379 password: database: 0 timeout: 2000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 cache: type: redis redis: time-to-live: 600000 # 10分钟 cache-null-values: true use-key-prefix: true key-prefix: "myapp:"
3. 配置 RedisCacheManager
java@Configuration @EnableCaching public class RedisCacheConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { // 默认配置 RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair n .fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues(); // 为不同缓存配置不同过期时间 Map<String, RedisCacheConfiguration> configMap = new HashMap<>(); configMap.put("users", defaultConfig.entryTtl(Duration.ofMinutes(30))); configMap.put("products", defaultConfig.entryTtl(Duration.ofMinutes(10))); configMap.put("orders", defaultConfig.entryTtl(Duration.ofMinutes(5))); configMap.put("shortTerm", defaultConfig.entryTtl(Duration.ofSeconds(60))); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(defaultConfig) .withInitialCacheConfigurations(configMap) .transactionAware() .build(); } /** * 配置 RedisTemplate */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); // 使用 Jackson 序列化 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } }
缓存注解详解
@Cacheable
java/** * 缓存查询结果 */ @Cacheable( value = "users", // 缓存名称 key = "#id", // 缓存键 condition = "#id > 0", // 条件:id>0 时才缓存 unless = "#result == null" // 结果不为 null 时才缓存 ) public User getUser(Long id) { return userRepository.findById(id).orElse(null); } /** * 使用 SpEL 表达式 */ @Cacheable(value = "users", key = "T(com.example.util.CacheKeyUtil).generateKey('user', #id)") public User getUserWithCustomKey(Long id) { return userRepository.findById(id).orElse(null); } /** * 多条件查询缓存 */ @Cacheable(value = "users", key = "#root.methodName + ':' + #username + ':' + #status") public List<User> findByUsernameAndStatus(String username, Integer status) { return userRepository.findByUsernameAndStatus(username, status); }
@CachePut
java/** * 更新缓存(方法执行后缓存) */ @CachePut(value = "users", key = "#user.id") public User updateUser(User user) { return userRepository.save(user); } /** * 根据结果动态设置 key */ @CachePut(value = "users", key = "#result.id") public User createUser(User user) { return userRepository.save(user); }
@CacheEvict
java/** * 删除指定缓存 */ @CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { userRepository.deleteById(id); } /** * 方法执行前删除缓存 */ @CacheEvict(value = "users", key = "#id", beforeInvocation = true) public void deleteUserBefore(Long id) { userRepository.deleteById(id); } /** * 清空整个缓存区域 */ @CacheEvict(value = "users", allEntries = true) public void clearAllUsers() { // 清空所有用户缓存 } /** * 同时清除多个缓存 */ @Caching(evict = { @CacheEvict(value = "users", key = "#user.id"), @CacheEvict(value = "users", key = "#user.username"), @CacheEvict(value = "userList", allEntries = true) }) public void deleteUserAndClearCache(User user) { userRepository.delete(user); }
自定义缓存 Key 生成器
java@Component public class CustomKeyGenerator implements KeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getSimpleName()).append(":"); sb.append(method.getName()).append(":"); for (Object param : params) { if (param != null) { sb.append(param.toString()).append(":"); } } return sb.toString(); } } // 使用 @Cacheable(value = "users", keyGenerator = "customKeyGenerator") public User getUser(Long id) { return userRepository.findById(id).orElse(null); }
缓存条件判断
java@Service public class ProductService { /** * 只有 VIP 用户的查询结果才缓存 */ @Cacheable( value = "products", key = "#userId", condition = "@userService.isVip(#userId)" ) public List<Product> getRecommendedProducts(Long userId) { // 获取推荐商品 return productRepository.findRecommendedByUserId(userId); } /** * 结果为空时不缓存 */ @Cacheable( value = "products", key = "#id", unless = "#result == null or #result.stock <= 0" ) public Product getProduct(Long id) { return productRepository.findById(id).orElse(null); } }
缓存与事务
java@Service public class OrderService { /** * 缓存与事务结合 * 注意:@Cacheable 在事务之外执行 */ @Transactional @Cacheable(value = "orders", key = "#orderId") public Order getOrder(Long orderId) { return orderRepository.findById(orderId).orElse(null); } /** * 更新操作:先更新数据库,再更新缓存 */ @Transactional @CachePut(value = "orders", key = "#order.id") public Order updateOrder(Order order) { // 业务校验 validateOrder(order); // 更新数据库 Order saved = orderRepository.save(order); // 发送消息 eventPublisher.publishEvent(new OrderUpdatedEvent(saved)); return saved; } }
缓存监控
java@Component @Slf4j public class CacheMonitor { @Autowired private CacheManager cacheManager; @Scheduled(fixedRate = 60000) // 每分钟执行 public void logCacheStats() { if (cacheManager instanceof CaffeineCacheManager) { Collection<String> cacheNames = cacheManager.getCacheNames(); for (String cacheName : cacheNames) { Cache cache = cacheManager.getCache(cacheName); if (cache instanceof CaffeineCache) { com.github.benmanes.caffeine.cache.Cache nativeCache = (com.github.benmanes.caffeine.cache.Cache) cache.getNativeCache(); log.info("Cache [{}] Stats: {}", cacheName, nativeCache.stats()); } } } } }
缓存穿透、击穿、雪崩解决方案
java@Service public class CacheSolutionService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private RedissonClient redissonClient; /** * 解决缓存穿透:缓存空值 */ @Cacheable( value = "users", key = "#id", unless = "#result == null" ) public User getUserWithNullCache(Long id) { User user = userRepository.findById(id).orElse(null); // 数据库不存在也缓存空值(短时间) if (user == null) { redisTemplate.opsForValue().set( "users::" + id, "null", 5, TimeUnit.MINUTES ); } return user; } /** * 解决缓存击穿:互斥锁 */ public User getUserWithLock(Long id) { String cacheKey = "users::" + id; String lockKey = "lock:users:" + id; // 先查缓存 String cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return JSON.parseObject(cached, User.class); } // 获取分布式锁 RLock lock = redissonClient.getLock(lockKey); try { // 尝试获取锁,最多等待 10 秒 boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (locked) { try { // 双重检查 cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return JSON.parseObject(cached, User.class); } // 查询数据库 User user = userRepository.findById(id).orElse(null); // 写入缓存 if (user != null) { redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(user), 30, TimeUnit.MINUTES ); } return user; } finally { lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return null; } /** * 解决缓存雪崩:随机过期时间 */ @Cacheable(value = "products", key = "#categoryId") public List<Product> getProductsByCategory(Long categoryId) { List<Product> products = productRepository.findByCategoryId(categoryId); // 设置随机过期时间,防止同时过期 int randomExpire = 600 + (int)(Math.random() * 300); // 10-15 分钟 redisTemplate.opsForValue().set( "products::" + categoryId, JSON.toJSONString(products), randomExpire, TimeUnit.SECONDS ); return products; } }
总结
| 缓存类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| ConcurrentMapCache | 单机、开发测试 | 简单、无需额外依赖 | 不支持分布式 |
| Caffeine | 单机高性能 | 性能极高、功能丰富 | 不支持分布式 |
| Redis | 分布式系统 | 支持分布式、持久化 | 网络开销 |
| EhCache | 企业级应用 | 功能全面、支持集群 | 配置复杂 |
选择建议:
- 单机应用:Caffeine
- 分布式应用:Redis
- 简单场景:ConcurrentMapCache