缓存使用最佳实践指南

什么地方加缓存 什么地方不加缓存 我在使用的时候发现缓存并不是在所有地方都加合适 有些增删改查比较频繁的地方 加上缓存 无疑是给自己加了层数据同步的麻烦 我引入缓存的初衷是 在初次打开页面时 比如推荐内容的页面 如果数据量过大 可能不能实现秒开 使用定时任务在凌晨为用户添加推荐内容的缓存 使得用户在打开页面时 能够有较好的体验 而像是这种增删改查队伍 增删改查用户的场景 加上缓存后 反而会让体验感下降 让用户觉得自己的修改没有生效 如果实在要兼顾两者 就得自己加上消息队列等同步机制 增加编程复杂性 简单的同步方法是 一旦涉及更新(增删改)操作 就清除缓存

一、核心原则:什么时候该用缓存

✅ 适合使用缓存的场景

/**
 * 缓存适用场景判断标准
 */
public class CacheScenarioGuide {

    // 1. 读多写少(读写比 > 10:1)
    // 2. 数据变化频率低
    // 3. 计算成本高
    // 4. 允许短暂的数据不一致
    // 5. 热点数据访问

    /**
     * ✅ 推荐场景1: 推荐内容(你的使用场景)
     * - 凌晨定时生成
     * - 用户打开秒级响应
     * - 数据允许延迟到下一次定时刷新
     */
    @Cacheable(value = "recommendations", key = "#userId")
    public List<RecommendContent> 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<DictData> getDictData(String dictType) {
        return dictRepository.findByType(dictType);
    }

    /**
     * ✅ 推荐场景4: 热门排行榜
     * - 计算复杂
     * - 允许延迟更新
     */
    @Cacheable(value = "ranking:hot", key = "#category")
    public List<Article> getHotArticles(String category) {
        return articleRepository.findHotByCategory(category);
    }

    /**
     * ✅ 推荐场景5: 统计数据
     * - 计算耗时
     * - 实时性要求不高
     */
    @Cacheable(value = "statistics", key = "#date")
    public DashboardStatistics getDailyStatistics(LocalDate date) {
        return calculateStatistics(date);
    }
}

❌ 不适合使用缓存的场景

/**
 * 不应该使用缓存的场景
 */
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<Message> getChatMessages(Long chatId) {
        return messageRepository.findByChatId(chatId);
    }
}

二、缓存更新策略

策略1: Cache Aside(旁路缓存)- 最常用

/**
 * 旁路缓存模式 - 推荐使用
 * 读:先查缓存,miss则查DB并写入缓存
 * 写:先更新DB,再删除缓存
 */
@Service
@Slf4j
public class CacheAsideService {

    @Autowired
    private RedisTemplate<String, Object> 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: 定时刷新(适合你的推荐场景)

/**
 * 定时刷新策略 - 适合推荐内容
 */
@Service
@Slf4j
public class RecommendationCacheService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private RecommendationEngine recommendationEngine;

    /**
     * 凌晨定时任务:预热推荐内容缓存
     */
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
    public void warmUpRecommendations() {
        log.info("开始预热推荐内容缓存");

        // 获取活跃用户列表
        List<Long> activeUserIds = getActiveUsers();

        int successCount = 0;
        for (Long userId : activeUserIds) {
            try {
                // 计算推荐内容
                List<Content> 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<Content> getUserRecommendations(Long userId) {
        String cacheKey = "recommendations:" + userId;

        @SuppressWarnings("unchecked")
        List<Content> cached = (List<Content>) redisTemplate.opsForValue().get(cacheKey);

        if (cached != null) {
            return cached;
        }

        // 缓存未命中(新用户或缓存过期),实时计算
        log.warn("推荐缓存未命中,实时计算: userId={}", userId);
        List<Content> recommendations = recommendationEngine.calculate(userId);

        // 写入缓存
        redisTemplate.opsForValue().set(cacheKey, recommendations, 24, TimeUnit.HOURS);

        return recommendations;
    }

    /**
     * 获取最近7天活跃的用户
     */
    private List<Long> getActiveUsers() {
        // 实现逻辑...
        return Collections.emptyList();
    }
}

策略3: Write Through(同步写入)- 谨慎使用

/**
 * 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: 你的推荐系统

/**
 * 推荐系统缓存方案
 */
@Service
@Slf4j
public class RecommendationService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String CACHE_KEY_PREFIX = "rec:";
    private static final int CACHE_HOURS = 24;

    /**
     * 用户获取推荐内容(秒开)
     */
    public List<Content> getRecommendations(Long userId) {
        String cacheKey = CACHE_KEY_PREFIX + userId;

        // 直接从缓存读取
        @SuppressWarnings("unchecked")
        List<Content> cached = (List<Content>) 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<Content> newRec = calculateRecommendations(userId);

        // 更新缓存
        redisTemplate.opsForValue().set(cacheKey, newRec, CACHE_HOURS, TimeUnit.HOURS);

        log.info("手动刷新推荐缓存: userId={}", userId);
    }

    private List<Content> calculateRecommendations(Long userId) {
        // 复杂推荐算法
        return Collections.emptyList();
    }

    private List<Content> getHotContents() {
        // 热门内容降级方案
        return Collections.emptyList();
    }

    private void recordUserBehavior(Long userId, List<Content> contents) {
        // 异步记录
    }
}

场景2: 队伍CRUD(不使用缓存)

/**
 * 队伍服务 - 直接访问数据库
 * 不使用缓存,保证数据实时性
 */
@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: 混合模式 - 配置中心

/**
 * 配置中心 - 缓存 + 实时更新通知
 */
@Service
@Slf4j
public class ConfigCenterService {

    @Autowired
    private RedisTemplate<String, Object> 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 - 应该做的

/**
 * 缓存最佳实践
 */
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 - 不应该做的

/**
 * 缓存反模式
 */
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<String> 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);
        // 降级处理
    }
}

五、决策流程图

开始
  │
  ├─ 数据是否频繁变化?
  │   ├─ 是(几秒/几分钟变一次)→ ❌ 不使用缓存
  │   └─ 否 → 继续
  │
  ├─ 是否需要强一致性?
  │   ├─ 是(订单、库存、支付)→ ❌ 不使用缓存
  │   └─ 否 → 继续
  │
  ├─ 读写比例?
  │   ├─ < 3:1 → ❌ 缓存收益低
  │   └─ >= 10:1 → ✅ 强烈推荐缓存
  │
  ├─ 计算成本?
  │   ├─ 高(>100ms)→ ✅ 推荐缓存
  │   └─ 低(<10ms)→ 缓存意义不大
  │
  └─ 选择缓存策略
      ├─ 定时生成类(推荐内容)→ 定时刷新策略
      ├─ 配置类数据 → Cache Aside + 长过期时间
      ├─ 热点数据 → 本地缓存 + Redis
      └─ 一般查询 → Cache Aside + 短过期时间

六、实际应用建议

# 缓存使用矩阵

✅ 推荐使用缓存:
  - 用户推荐内容 (定时刷新, 24h过期)
  - 热门队伍列表 (10min过期)
  - 系统配置 (1h过期)
  - 字典数据 (永久, 手动更新时删除)
  - 统计数据 (5min过期)
  - 标签列表 (30min过期)

❌ 不使用缓存:
  - 用户CRUD (实时性要求高)
  - 队伍CRUD (成员变化需实时反馈)
  - 聊天消息 (必须实时)
  - 匹配状态 (状态变化需立即体现)
  - 通知消息 (实时推送)

⚠️ 谨慎使用缓存:
  - 用户在线状态 (可以用5s-10s过期的缓存)
  - 文章阅读数 (可以异步更新, 允许短暂不一致)
  - 点赞数 (可以用1min过期的缓存)

监控指标

@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 | 01-缓存使用最佳实践指南 | ➡️ 02-Spring Boot 跨域(CORS)问题