--- title: "01-缓存使用最佳实践指南" created: 2025-12-02 tags: - 项目 aliases: - 缓存使用最佳实践指南 --- # 缓存使用最佳实践指南 什么地方加缓存 什么地方不加缓存 我在使用的时候发现缓存并不是在所有地方都加合适 有些增删改查比较频繁的地方 加上缓存 无疑是给自己加了层数据同步的麻烦 我引入缓存的初衷是 在初次打开页面时 比如推荐内容的页面 如果数据量过大 可能不能实现秒开 使用定时任务在凌晨为用户添加推荐内容的缓存 使得用户在打开页面时 能够有较好的体验 而像是这种增删改查队伍 增删改查用户的场景 加上缓存后 反而会让体验感下降 让用户觉得自己的修改没有生效 如果实在要兼顾两者 就得自己加上消息队列等同步机制 增加编程复杂性 简单的同步方法是 一旦涉及更新(增删改)操作 就清除缓存 ## **一、核心原则:什么时候该用缓存** ### **✅ 适合使用缓存的场景** ```java /** * 缓存适用场景判断标准 */ public class CacheScenarioGuide { // 1. 读多写少(读写比 > 10:1) // 2. 数据变化频率低 // 3. 计算成本高 // 4. 允许短暂的数据不一致 // 5. 热点数据访问 /** * ✅ 推荐场景1: 推荐内容(你的使用场景) * - 凌晨定时生成 * - 用户打开秒级响应 * - 数据允许延迟到下一次定时刷新 */ @Cacheable(value = "recommendations", key = "#userId") public List getUserRecommendations(Long userId) { // 复杂的推荐算法计算 return calculateRecommendations(userId); } /** * ✅ 推荐场景2: 配置信息 * - 很少变化 * - 全局共享 * - 频繁读取 */ @Cacheable(value = "system:config", key = "#configKey") public String getSystemConfig(String configKey) { return configRepository.findByKey(configKey); } /** * ✅ 推荐场景3: 字典数据 * - 基本不变 * - 高频访问 */ @Cacheable(value = "dict", key = "#dictType") public List getDictData(String dictType) { return dictRepository.findByType(dictType); } /** * ✅ 推荐场景4: 热门排行榜 * - 计算复杂 * - 允许延迟更新 */ @Cacheable(value = "ranking:hot", key = "#category") public List
getHotArticles(String category) { return articleRepository.findHotByCategory(category); } /** * ✅ 推荐场景5: 统计数据 * - 计算耗时 * - 实时性要求不高 */ @Cacheable(value = "statistics", key = "#date") public DashboardStatistics getDailyStatistics(LocalDate date) { return calculateStatistics(date); } } ``` ### **❌ 不适合使用缓存的场景** ```java /** * 不应该使用缓存的场景 */ public class AntiCachePatterns { /** * ❌ 反例1: 用户CRUD操作 * 问题:用户修改后看不到最新数据,体验差 */ // 不要这样做! @Cacheable(value = "user", key = "#userId") public User getUserById(Long userId) { return userRepository.findById(userId); } /** * ❌ 反例2: 队伍/团队CRUD * 问题:成员加入、退出需要实时反馈 */ // 不要这样做! @Cacheable(value = "team", key = "#teamId") public Team getTeamById(Long teamId) { return teamRepository.findById(teamId); } /** * ❌ 反例3: 订单状态 * 问题:支付、发货等状态必须实时 */ // 不要这样做! @Cacheable(value = "order", key = "#orderId") public Order getOrderStatus(Long orderId) { return orderRepository.findById(orderId); } /** * ❌ 反例4: 库存数量 * 问题:并发问题,可能超卖 */ // 不要这样做! @Cacheable(value = "stock", key = "#productId") public Integer getStock(Long productId) { return stockRepository.getStock(productId); } /** * ❌ 反例5: 实时聊天消息 * 问题:消息必须实时送达 */ // 不要这样做! @Cacheable(value = "messages", key = "#chatId") public List getChatMessages(Long chatId) { return messageRepository.findByChatId(chatId); } } ``` ## **二、缓存更新策略** ### **策略1: Cache Aside(旁路缓存)- 最常用** ```java /** * 旁路缓存模式 - 推荐使用 * 读:先查缓存,miss则查DB并写入缓存 * 写:先更新DB,再删除缓存 */ @Service @Slf4j public class CacheAsideService { @Autowired private RedisTemplate redisTemplate; @Autowired private UserRepository userRepository; /** * 读操作:查询用户信息(不常变的部分) */ public User getUserProfile(Long userId) { String cacheKey = "user:profile:" + userId; // 1. 先查缓存 User user = (User) redisTemplate.opsForValue().get(cacheKey); if (user != null) { log.info("缓存命中: {}", cacheKey); return user; } // 2. 缓存miss,查询数据库 user = userRepository.findById(userId).orElse(null); if (user != null) { // 3. 写入缓存(设置过期时间) redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS); log.info("写入缓存: {}", cacheKey); } return user; } /** * 写操作:更新用户信息 * 策略:先更新DB,再删除缓存(推荐) */ public void updateUserProfile(Long userId, User user) { // 1. 先更新数据库 userRepository.save(user); // 2. 删除缓存(而不是更新缓存) String cacheKey = "user:profile:" + userId; redisTemplate.delete(cacheKey); log.info("删除缓存: {}", cacheKey); // 下次查询时会重新加载最新数据 } /** * 为什么删除而不是更新? * 1. 更新可能失败,导致缓存与DB不一致 * 2. 如果更新后短期内无人访问,更新操作浪费 * 3. 删除后懒加载,减少不必要的缓存写入 */ } ``` ### **策略2: 定时刷新(适合你的推荐场景)** ```java /** * 定时刷新策略 - 适合推荐内容 */ @Service @Slf4j public class RecommendationCacheService { @Autowired private RedisTemplate redisTemplate; @Autowired private RecommendationEngine recommendationEngine; /** * 凌晨定时任务:预热推荐内容缓存 */ @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点 public void warmUpRecommendations() { log.info("开始预热推荐内容缓存"); // 获取活跃用户列表 List activeUserIds = getActiveUsers(); int successCount = 0; for (Long userId : activeUserIds) { try { // 计算推荐内容 List recommendations = recommendationEngine.calculate(userId); // 写入缓存,设置24小时过期 String cacheKey = "recommendations:" + userId; redisTemplate.opsForValue().set( cacheKey, recommendations, 24, TimeUnit.HOURS ); successCount++; } catch (Exception e) { log.error("用户 {} 推荐内容缓存失败", userId, e); } } log.info("推荐内容缓存预热完成: {}/{}", successCount, activeUserIds.size()); } /** * 用户访问时直接读取缓存 */ public List getUserRecommendations(Long userId) { String cacheKey = "recommendations:" + userId; @SuppressWarnings("unchecked") List cached = (List) redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return cached; } // 缓存未命中(新用户或缓存过期),实时计算 log.warn("推荐缓存未命中,实时计算: userId={}", userId); List recommendations = recommendationEngine.calculate(userId); // 写入缓存 redisTemplate.opsForValue().set(cacheKey, recommendations, 24, TimeUnit.HOURS); return recommendations; } /** * 获取最近7天活跃的用户 */ private List getActiveUsers() { // 实现逻辑... return Collections.emptyList(); } } ``` ### **策略3: Write Through(同步写入)- 谨慎使用** ```java /** * Write Through 模式 * 适合:强一致性要求,但会增加写入延迟 */ @Service public class WriteThroughService { /** * ⚠️ 仅在必要时使用 * 同时更新缓存和数据库 */ @Transactional public void updateConfig(String key, String value) { // 1. 更新数据库 configRepository.updateValue(key, value); // 2. 同步更新缓存 String cacheKey = "config:" + key; redisTemplate.opsForValue().set(cacheKey, value); // 如果缓存更新失败,整个事务回滚 } } ``` ## **三、实战:基于场景的缓存方案** ### **场景1: 你的推荐系统** ```java /** * 推荐系统缓存方案 */ @Service @Slf4j public class RecommendationService { @Autowired private RedisTemplate redisTemplate; private static final String CACHE_KEY_PREFIX = "rec:"; private static final int CACHE_HOURS = 24; /** * 用户获取推荐内容(秒开) */ public List getRecommendations(Long userId) { String cacheKey = CACHE_KEY_PREFIX + userId; // 直接从缓存读取 @SuppressWarnings("unchecked") List cached = (List) redisTemplate.opsForValue().get(cacheKey); if (cached != null) { // 异步记录用户行为,用于下次推荐优化 recordUserBehavior(userId, cached); return cached; } // 降级方案:返回通用热门内容 return getHotContents(); } /** * 凌晨批量生成推荐(定时任务) */ @Scheduled(cron = "0 0 2 * * ?") public void batchGenerateRecommendations() { // 实现见上面的 warmUpRecommendations } /** * 手动刷新某个用户的推荐(管理员操作) */ public void refreshUserRecommendation(Long userId) { String cacheKey = CACHE_KEY_PREFIX + userId; // 重新计算 List newRec = calculateRecommendations(userId); // 更新缓存 redisTemplate.opsForValue().set(cacheKey, newRec, CACHE_HOURS, TimeUnit.HOURS); log.info("手动刷新推荐缓存: userId={}", userId); } private List calculateRecommendations(Long userId) { // 复杂推荐算法 return Collections.emptyList(); } private List getHotContents() { // 热门内容降级方案 return Collections.emptyList(); } private void recordUserBehavior(Long userId, List contents) { // 异步记录 } } ``` ### **场景2: 队伍CRUD(不使用缓存)** ```java /** * 队伍服务 - 直接访问数据库 * 不使用缓存,保证数据实时性 */ @Service @Slf4j public class TeamService { @Autowired private TeamRepository teamRepository; /** * ✅ 直接查询数据库,无缓存 */ public Team getTeamById(Long teamId) { return teamRepository.findById(teamId) .orElseThrow(() -> new ResourceNotFoundException("队伍不存在")); } /** * ✅ 直接更新数据库 */ @Transactional public void updateTeam(Long teamId, TeamUpdateDTO dto) { Team team = getTeamById(teamId); // 更新字段 BeanUtils.copyProperties(dto, team); teamRepository.save(team); log.info("队伍更新成功: teamId={}", teamId); // 无需处理缓存 } /** * ✅ 加入队伍 - 实时生效 */ @Transactional public void joinTeam(Long teamId, Long userId) { Team team = getTeamById(teamId); // 业务逻辑... team.addMember(userId); teamRepository.save(team); log.info("用户加入队伍: teamId={}, userId={}", teamId, userId); // 用户立即能看到变化 } } ``` ### **场景3: 混合模式 - 配置中心** ```java /** * 配置中心 - 缓存 + 实时更新通知 */ @Service @Slf4j public class ConfigCenterService { @Autowired private RedisTemplate redisTemplate; @Autowired private ConfigRepository configRepository; @Autowired private ApplicationEventPublisher eventPublisher; private static final String CACHE_PREFIX = "config:"; /** * 读取配置(优先缓存) */ public String getConfig(String key) { String cacheKey = CACHE_PREFIX + key; // 1. 查缓存 String value = (String) redisTemplate.opsForValue().get(cacheKey); if (value != null) { return value; } // 2. 查数据库 Config config = configRepository.findByKey(key); if (config != null) { value = config.getValue(); // 3. 写入缓存(永久,因为配置很少变) redisTemplate.opsForValue().set(cacheKey, value); } return value; } /** * 更新配置(清除缓存 + 发布事件) */ @Transactional public void updateConfig(String key, String value) { // 1. 更新数据库 Config config = configRepository.findByKey(key); if (config == null) { config = new Config(key, value); } else { config.setValue(value); } configRepository.save(config); // 2. 删除缓存 String cacheKey = CACHE_PREFIX + key; redisTemplate.delete(cacheKey); // 3. 发布配置变更事件(分布式场景) eventPublisher.publishEvent(new ConfigChangedEvent(key, value)); log.info("配置更新: key={}, value={}", key, value); } /** * 监听配置变更事件(集群环境) */ @EventListener public void onConfigChanged(ConfigChangedEvent event) { // 清除本地缓存 String cacheKey = CACHE_PREFIX + event.getKey(); redisTemplate.delete(cacheKey); log.info("收到配置变更通知: key={}", event.getKey()); } } ``` ## **四、缓存最佳实践清单** ### **✅ DO - 应该做的** ```java /** * 缓存最佳实践 */ public class CacheBestPractices { // 1. 始终设置过期时间,防止内存泄漏 redisTemplate.opsForValue().set(key, value, 1, TimeUnit.HOURS); // 2. 使用合理的key命名规范 String key = "domain:type:id"; // 例如: user:profile:12345 // 3. 缓存空值,防止缓存穿透 if (user == null) { redisTemplate.opsForValue().set(key, NULL_VALUE, 5, TimeUnit.MINUTES); } // 4. 使用布隆过滤器,防止大量不存在的key查询 @Autowired private RedisBloomFilter bloomFilter; public User getUser(Long userId) { if (!bloomFilter.contains("user:" + userId)) { return null; // 用户不存在,直接返回 } // 继续查询... } // 5. 热点数据单独处理 if (isHotData(key)) { // 使用本地缓存 + Redis二级缓存 return localCache.get(key, () -> redis.get(key)); } // 6. 缓存预热 @PostConstruct public void warmUp() { // 启动时加载热点数据 } // 7. 监控缓存命中率 @Scheduled(fixedRate = 60000) public void logCacheStats() { log.info("缓存命中率: {}%", calculateHitRate()); } } ``` ### **❌ DON'T - 不应该做的** ```java /** * 缓存反模式 */ public class CacheAntiPatterns { // ❌ 1. 不要缓存大对象 // Bad: redisTemplate.opsForValue().set(key, hugeObject); // 10MB+ // Good: redisTemplate.opsForValue().set(key, hugeObject.getSummary()); // ❌ 2. 不要设置永久缓存 // Bad: redisTemplate.opsForValue().set(key, value); // 永不过期 // Good: redisTemplate.opsForValue().set(key, value, 24, TimeUnit.HOURS); // ❌ 3. 不要在循环中访问缓存 // Bad: for (Long id : ids) { User user = (User) redisTemplate.opsForValue().get("user:" + id); } // Good: List keys = ids.stream() .map(id -> "user:" + id) .collect(Collectors.toList()); List users = redisTemplate.opsForValue().multiGet(keys); // ❌ 4. 不要缓存敏感信息 // Bad: redisTemplate.opsForValue().set("user:password:" + userId, password); // ❌ 5. 不要忽略缓存失败 // Bad: try { redisTemplate.opsForValue().set(key, value); } catch (Exception e) { // 静默失败 } // Good: try { redisTemplate.opsForValue().set(key, value); } catch (Exception e) { log.error("缓存写入失败: key={}", key, e); // 降级处理 } } ``` ## **五、决策流程图** ```text 开始 │ ├─ 数据是否频繁变化? │ ├─ 是(几秒/几分钟变一次)→ ❌ 不使用缓存 │ └─ 否 → 继续 │ ├─ 是否需要强一致性? │ ├─ 是(订单、库存、支付)→ ❌ 不使用缓存 │ └─ 否 → 继续 │ ├─ 读写比例? │ ├─ < 3:1 → ❌ 缓存收益低 │ └─ >= 10:1 → ✅ 强烈推荐缓存 │ ├─ 计算成本? │ ├─ 高(>100ms)→ ✅ 推荐缓存 │ └─ 低(<10ms)→ 缓存意义不大 │ └─ 选择缓存策略 ├─ 定时生成类(推荐内容)→ 定时刷新策略 ├─ 配置类数据 → Cache Aside + 长过期时间 ├─ 热点数据 → 本地缓存 + Redis └─ 一般查询 → Cache Aside + 短过期时间 ``` ## **六、实际应用建议** ```text # 缓存使用矩阵 ✅ 推荐使用缓存: - 用户推荐内容 (定时刷新, 24h过期) - 热门队伍列表 (10min过期) - 系统配置 (1h过期) - 字典数据 (永久, 手动更新时删除) - 统计数据 (5min过期) - 标签列表 (30min过期) ❌ 不使用缓存: - 用户CRUD (实时性要求高) - 队伍CRUD (成员变化需实时反馈) - 聊天消息 (必须实时) - 匹配状态 (状态变化需立即体现) - 通知消息 (实时推送) ⚠️ 谨慎使用缓存: - 用户在线状态 (可以用5s-10s过期的缓存) - 文章阅读数 (可以异步更新, 允许短暂不一致) - 点赞数 (可以用1min过期的缓存) ``` ### **监控指标** ```java @Component @Slf4j public class CacheMonitor { private AtomicLong hits = new AtomicLong(0); private AtomicLong misses = new AtomicLong(0); public void recordHit() { hits.incrementAndGet(); } public void recordMiss() { misses.incrementAndGet(); } @Scheduled(fixedRate = 60000) // 每分钟输出 public void reportStats() { long totalHits = hits.get(); long totalMisses = misses.get(); long total = totalHits + totalMisses; if (total > 0) { double hitRate = (double) totalHits / total * 100; log.info("缓存统计 - 命中率: {:.2f}%, 命中: {}, 未命中: {}", hitRate, totalHits, totalMisses); // 命中率低于50%,可能缓存策略有问题 if (hitRate < 50) { log.warn("⚠️ 缓存命中率过低,建议检查缓存策略"); } } // 重置计数器 hits.set(0); misses.set(0); } } ``` **缓存应该用在"提升体验"而不是"增加复杂度"的地方。** **不确定时,不加缓存;确定收益大时,再加缓存。** --- **项目分区导航**:⬅️ [[01-Knife4j OpenAPI 3.0 完整配置指南|01-Knife4j OpenAPI 3]] | 01-缓存使用最佳实践指南 | ➡️ [[02-Spring Boot 跨域(CORS)问题|02-Spring Boot 跨域(CORS)问题]]