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