🚀 第15章 SpringBoot Cache

掌握缓存技术,提升应用性能和用户体验

💻 查看完整代码 - 在线IDE体验

📋 学习目标

  • 理解缓存概念 - 缓存原理与应用场景
  • 掌握Spring Cache - 注解式缓存使用
  • 集成Redis缓存 - 分布式缓存方案
  • 缓存策略配置 - 过期时间、淘汰策略
  • 缓存最佳实践 - 缓存穿透、雪崩处理
  • 性能优化技巧 - 缓存预热、监控

🔍 缓存核心概念

  • 缓存命中 - 数据在缓存中找到
  • 缓存穿透 - 查询不存在的数据
  • 缓存雪崩 - 大量缓存同时失效
  • 缓存击穿 - 热点数据缓存失效
  • 缓存预热 - 提前加载热点数据
  • 缓存更新 - 数据一致性保证

📦 缓存依赖配置

<!-- pom.xml 缓存依赖配置 --> <dependencies> <!-- SpringBoot Cache启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!-- SpringBoot Redis启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- SpringBoot Web启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- JSON处理 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies> # application.yml 缓存配置 spring: # Redis配置 redis: host: localhost port: 6379 password: database: 0 timeout: 2000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: -1ms # 缓存配置 cache: type: redis redis: time-to-live: 600000 # 10分钟 cache-null-values: true use-key-prefix: true key-prefix: "cache:" # 日志配置 logging: level: cn.bugstack.springframework.boot.cache: DEBUG org.springframework.cache: DEBUG pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

🔄 缓存执行流程

1
请求到达
客户端发起数据查询请求
2
缓存查找
检查缓存中是否存在数据
3
缓存命中
如果命中,直接返回缓存数据
4
缓存未命中
查询数据库获取数据
5
更新缓存
将查询结果存入缓存

🎯 Spring Cache注解使用

// CacheConfig.java - 缓存配置类 @Configuration @EnableCaching @Slf4j public class CacheConfig { /** * 配置Redis缓存管理器 */ @Bean @Primary public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() // 设置缓存过期时间为10分钟 .entryTtl(Duration.ofMinutes(10)) // 设置key序列化方式 .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) // 设置value序列化方式 .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())) // 不缓存null值 .disableCachingNullValues(); return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(config) .build(); } /** * 配置本地缓存管理器(Caffeine) */ @Bean("caffeineCacheManager") public CacheManager caffeineCacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() // 设置最大缓存数量 .maximumSize(1000) // 设置写入后过期时间 .expireAfterWrite(Duration.ofMinutes(5)) // 设置缓存移除监听器 .removalListener((key, value, cause) -> log.info("缓存移除 - key: {}, value: {}, cause: {}", key, value, cause)) ); return cacheManager; } } // UserService.java - 用户服务类 @Service @Slf4j public class UserService { @Autowired private UserRepository userRepository; /** * @Cacheable - 缓存查询结果 * value: 缓存名称 * key: 缓存key,支持SpEL表达式 * condition: 缓存条件 * unless: 排除条件 */ @Cacheable(value = "users", key = "#id", condition = "#id > 0") public User getUserById(Long id) { log.info("从数据库查询用户信息,ID: {}", id); // 模拟数据库查询耗时 try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } User user = userRepository.findById(id).orElse(null); if (user == null) { log.warn("用户不存在,ID: {}", id); } return user; } /** * @CachePut - 更新缓存 * 无论缓存是否存在,都会执行方法并更新缓存 */ @CachePut(value = "users", key = "#user.id") public User saveUser(User user) { log.info("保存用户信息: {}", user); if (user.getId() == null) { user.setId(System.currentTimeMillis()); } user.setUpdateTime(LocalDateTime.now()); User savedUser = userRepository.save(user); log.info("用户保存成功,更新缓存: {}", savedUser); return savedUser; } /** * @CacheEvict - 清除缓存 * allEntries: 是否清除所有缓存条目 * beforeInvocation: 是否在方法执行前清除缓存 */ @CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { log.info("删除用户,ID: {}", id); userRepository.deleteById(id); log.info("用户删除成功,清除缓存: {}", id); } /** * @CacheEvict - 清除所有用户缓存 */ @CacheEvict(value = "users", allEntries = true) public void clearAllUserCache() { log.info("清除所有用户缓存"); } /** * @Caching - 组合多个缓存操作 */ @Caching( cacheable = @Cacheable(value = "userStats", key = "#userId"), put = @CachePut(value = "userLastAccess", key = "#userId") ) public UserStats getUserStats(Long userId) { log.info("获取用户统计信息,ID: {}", userId); // 模拟复杂统计计算 UserStats stats = new UserStats(); stats.setUserId(userId); stats.setLoginCount(100); stats.setLastLoginTime(LocalDateTime.now()); stats.setTotalPoints(1500); return stats; } } // User.java - 用户实体类 @Data @NoArgsConstructor @AllArgsConstructor @Entity @Table(name = "users") public class User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String username; @Column(nullable = false) private String email; private String nickname; @Column(name = "create_time") private LocalDateTime createTime; @Column(name = "update_time") private LocalDateTime updateTime; @PrePersist protected void onCreate() { createTime = LocalDateTime.now(); updateTime = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updateTime = LocalDateTime.now(); } } // UserStats.java - 用户统计实体 @Data @NoArgsConstructor @AllArgsConstructor public class UserStats implements Serializable { private Long userId; private Integer loginCount; private LocalDateTime lastLoginTime; private Integer totalPoints; }

🎨 缓存架构图示

L1 Cache
本地缓存
L2 Cache
Redis缓存
Database
数据库

多级缓存架构:本地缓存 → Redis缓存 → 数据库

提供更好的性能和可用性保障

📍 缓存注解

  • @Cacheable - 缓存查询结果
  • @CachePut - 更新缓存数据
  • @CacheEvict - 清除缓存数据
  • @Caching - 组合多个缓存操作
  • @CacheConfig - 类级别缓存配置
  • @EnableCaching - 启用缓存功能

📢 缓存策略

  • Cache-Aside - 旁路缓存模式
  • Read-Through - 读穿透模式
  • Write-Through - 写穿透模式
  • Write-Behind - 写回模式
  • Refresh-Ahead - 预刷新模式
  • TTL策略 - 时间过期策略

🔧 自定义缓存管理

// CustomCacheManager.java - 自定义缓存管理器 @Component @Slf4j public class CustomCacheManager { @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 设置缓存 */ public void set(String key, Object value, long timeout, TimeUnit unit) { try { redisTemplate.opsForValue().set(key, value, timeout, unit); log.debug("设置缓存成功 - key: {}, timeout: {}{}", key, timeout, unit); } catch (Exception e) { log.error("设置缓存失败 - key: {}", key, e); } } /** * 获取缓存 */ @SuppressWarnings("unchecked") public <T> T get(String key, Class<T> clazz) { try { Object value = redisTemplate.opsForValue().get(key); if (value == null) { log.debug("缓存未命中 - key: {}", key); return null; } log.debug("缓存命中 - key: {}", key); return clazz.isInstance(value) ? (T) value : null; } catch (Exception e) { log.error("获取缓存失败 - key: {}", key, e); return null; } } /** * 删除缓存 */ public boolean delete(String key) { try { Boolean result = redisTemplate.delete(key); log.debug("删除缓存 - key: {}, result: {}", key, result); return Boolean.TRUE.equals(result); } catch (Exception e) { log.error("删除缓存失败 - key: {}", key, e); return false; } } /** * 批量删除缓存 */ public long deletePattern(String pattern) { try { Set<String> keys = redisTemplate.keys(pattern); if (keys != null && !keys.isEmpty()) { Long result = redisTemplate.delete(keys); log.debug("批量删除缓存 - pattern: {}, count: {}", pattern, result); return result != null ? result : 0; } return 0; } catch (Exception e) { log.error("批量删除缓存失败 - pattern: {}", pattern, e); return 0; } } /** * 检查缓存是否存在 */ public boolean exists(String key) { try { Boolean result = redisTemplate.hasKey(key); return Boolean.TRUE.equals(result); } catch (Exception e) { log.error("检查缓存存在性失败 - key: {}", key, e); return false; } } /** * 设置缓存过期时间 */ public boolean expire(String key, long timeout, TimeUnit unit) { try { Boolean result = redisTemplate.expire(key, timeout, unit); log.debug("设置缓存过期时间 - key: {}, timeout: {}{}", key, timeout, unit); return Boolean.TRUE.equals(result); } catch (Exception e) { log.error("设置缓存过期时间失败 - key: {}", key, e); return false; } } /** * 获取缓存剩余过期时间 */ public long getExpire(String key, TimeUnit unit) { try { Long expire = redisTemplate.getExpire(key, unit); return expire != null ? expire : -1; } catch (Exception e) { log.error("获取缓存过期时间失败 - key: {}", key, e); return -1; } } } // CacheService.java - 缓存服务类 @Service @Slf4j public class CacheService { @Autowired private CustomCacheManager cacheManager; private static final String USER_CACHE_PREFIX = "user:"; private static final String PRODUCT_CACHE_PREFIX = "product:"; /** * 缓存用户信息 */ public void cacheUser(User user) { String key = USER_CACHE_PREFIX + user.getId(); cacheManager.set(key, user, 30, TimeUnit.MINUTES); } /** * 获取缓存的用户信息 */ public User getCachedUser(Long userId) { String key = USER_CACHE_PREFIX + userId; return cacheManager.get(key, User.class); } /** * 清除用户缓存 */ public void evictUser(Long userId) { String key = USER_CACHE_PREFIX + userId; cacheManager.delete(key); } /** * 清除所有用户缓存 */ public void evictAllUsers() { String pattern = USER_CACHE_PREFIX + "*"; long count = cacheManager.deletePattern(pattern); log.info("清除所有用户缓存,数量: {}", count); } /** * 缓存预热 - 预加载热点数据 */ @PostConstruct public void warmUpCache() { log.info("开始缓存预热..."); // 预加载热点用户数据 List<Long> hotUserIds = Arrays.asList(1L, 2L, 3L, 4L, 5L); for (Long userId : hotUserIds) { try { // 这里应该从数据库加载用户数据 User user = loadUserFromDatabase(userId); if (user != null) { cacheUser(user); log.debug("预热用户缓存: {}", userId); } } catch (Exception e) { log.error("预热用户缓存失败: {}", userId, e); } } log.info("缓存预热完成"); } /** * 从数据库加载用户数据(模拟) */ private User loadUserFromDatabase(Long userId) { // 模拟数据库查询 User user = new User(); user.setId(userId); user.setUsername("user" + userId); user.setEmail("user" + userId + "@example.com"); user.setCreateTime(LocalDateTime.now()); return user; } }

🛡️ 缓存问题解决方案

// CacheProblemSolver.java - 缓存问题解决方案 @Service @Slf4j public class CacheProblemSolver { @Autowired private CustomCacheManager cacheManager; @Autowired private UserRepository userRepository; /** * 解决缓存穿透问题 * 使用布隆过滤器或缓存空值 */ public User getUserWithBloomFilter(Long userId) { String key = "user:" + userId; // 1. 先检查缓存 User cachedUser = cacheManager.get(key, User.class); if (cachedUser != null) { return cachedUser; } // 2. 检查布隆过滤器(这里简化为ID范围检查) if (userId <= 0 || userId > 1000000) { log.warn("用户ID超出范围,可能是恶意请求: {}", userId); // 缓存空值,防止重复查询 cacheManager.set(key, new User(), 5, TimeUnit.MINUTES); return null; } // 3. 查询数据库 User user = userRepository.findById(userId).orElse(null); // 4. 缓存结果(包括空值) if (user != null) { cacheManager.set(key, user, 30, TimeUnit.MINUTES); } else { // 缓存空值,设置较短过期时间 cacheManager.set(key, new User(), 5, TimeUnit.MINUTES); } return user; } /** * 解决缓存雪崩问题 * 使用随机过期时间 */ public User getUserWithRandomExpire(Long userId) { String key = "user:" + userId; User cachedUser = cacheManager.get(key, User.class); if (cachedUser != null) { return cachedUser; } User user = userRepository.findById(userId).orElse(null); if (user != null) { // 设置随机过期时间,避免同时失效 int randomMinutes = 30 + new Random().nextInt(30); // 30-60分钟 cacheManager.set(key, user, randomMinutes, TimeUnit.MINUTES); } return user; } /** * 解决缓存击穿问题 * 使用分布式锁 */ public User getUserWithDistributedLock(Long userId) { String key = "user:" + userId; String lockKey = "lock:user:" + userId; // 1. 先检查缓存 User cachedUser = cacheManager.get(key, User.class); if (cachedUser != null) { return cachedUser; } // 2. 尝试获取分布式锁 boolean lockAcquired = acquireDistributedLock(lockKey, 10, TimeUnit.SECONDS); if (!lockAcquired) { // 获取锁失败,等待一段时间后重试 try { Thread.sleep(100); return cacheManager.get(key, User.class); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; } } try { // 3. 双重检查缓存 cachedUser = cacheManager.get(key, User.class); if (cachedUser != null) { return cachedUser; } // 4. 查询数据库 User user = userRepository.findById(userId).orElse(null); // 5. 更新缓存 if (user != null) { cacheManager.set(key, user, 30, TimeUnit.MINUTES); } return user; } finally { // 6. 释放分布式锁 releaseDistributedLock(lockKey); } } /** * 获取分布式锁(简化实现) */ private boolean acquireDistributedLock(String lockKey, long timeout, TimeUnit unit) { try { String lockValue = UUID.randomUUID().toString(); Boolean result = cacheManager.redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, timeout, unit); return Boolean.TRUE.equals(result); } catch (Exception e) { log.error("获取分布式锁失败: {}", lockKey, e); return false; } } /** * 释放分布式锁 */ private void releaseDistributedLock(String lockKey) { try { cacheManager.delete(lockKey); } catch (Exception e) { log.error("释放分布式锁失败: {}", lockKey, e); } } /** * 缓存更新策略 - 延时双删 */ @Transactional public User updateUserWithDelayedDoubleDelete(User user) { String key = "user:" + user.getId(); // 1. 删除缓存 cacheManager.delete(key); // 2. 更新数据库 User updatedUser = userRepository.save(user); // 3. 延时删除缓存(异步执行) CompletableFuture.runAsync(() -> { try { Thread.sleep(1000); // 延时1秒 cacheManager.delete(key); log.debug("延时双删完成: {}", key); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("延时双删被中断: {}", key); } }); return updatedUser; } }

📊 缓存方案对比

缓存类型 适用场景 优点 缺点
本地缓存 单机应用、读多写少 速度快、无网络开销 不支持分布式、内存限制
Redis缓存 分布式应用、高并发 支持分布式、功能丰富 网络开销、单点故障
多级缓存 高性能要求、大规模应用 性能最优、可用性高 复杂度高、一致性难保证
CDN缓存 静态资源、全球分发 就近访问、减轻服务器压力 只适用静态内容

💡 缓存最佳实践

🎯 设计原则

合理设置过期时间 - 根据数据特性设置TTL

避免缓存穿透 - 使用布隆过滤器或缓存空值

防止缓存雪崩 - 设置随机过期时间

解决缓存击穿 - 使用分布式锁或互斥锁

保证数据一致性 - 选择合适的缓存更新策略

// CacheMonitor.java - 缓存监控 @Component @Slf4j public class CacheMonitor { @Autowired private CacheManager cacheManager; /** * 缓存统计信息 */ @Scheduled(fixedRate = 60000) // 每分钟执行一次 public void logCacheStats() { if (cacheManager instanceof RedisCacheManager) { Collection<String> cacheNames = cacheManager.getCacheNames(); for (String cacheName : cacheNames) { Cache cache = cacheManager.getCache(cacheName); if (cache instanceof RedisCache) { log.info("缓存统计 - 缓存名: {}", cacheName); // 这里可以添加更详细的统计信息 } } } } /** * 缓存健康检查 */ @EventListener public void handleCacheEvent(CacheEvent event) { log.debug("缓存事件: {}", event); } } // 缓存配置建议 @Configuration public class CacheOptimizationConfig { /** * 配置缓存键生成器 */ @Bean public KeyGenerator customKeyGenerator() { return (target, method, params) -> { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getSimpleName()).append("."); sb.append(method.getName()).append("("); for (Object param : params) { sb.append(param.toString()).append(","); } if (params.length > 0) { sb.setLength(sb.length() - 1); } sb.append(")"); return sb.toString(); }; } /** * 配置缓存错误处理器 */ @Bean public CacheErrorHandler cacheErrorHandler() { return new CacheErrorHandler() { @Override public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) { log.error("缓存获取错误 - cache: {}, key: {}", cache.getName(), key, exception); } @Override public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) { log.error("缓存存储错误 - cache: {}, key: {}", cache.getName(), key, exception); } @Override public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) { log.error("缓存清除错误 - cache: {}, key: {}", cache.getName(), key, exception); } @Override public void handleCacheClearError(RuntimeException exception, Cache cache) { log.error("缓存清空错误 - cache: {}", cache.getName(), exception); } }; } }

🎉 恭喜完成第15章学习!

你已经掌握了SpringBoot Cache的核心技能,接下来学习应用启动和运行时管理!

🚀 下一章:SpringBoot Runtime 🏠 返回课程首页