1. 缓存穿透问题与解决方案
1.1 什么是缓存穿透
缓存穿透是指查询一个不存在的数据,由于缓存中没有这个数据,每次请求都会直接打到数据库。
如果有恶意用户不断请求不存在的数据,就会给数据库带来巨大压力。
这种情况下,缓存失去了保护数据库的作用。
典型场景:
- 用户查询一个不存在的商品ID
- 恶意攻击者故意查询大量无效数据
- 业务逻辑错误导致的无效查询
1.2 布隆过滤器解决方案
布隆过滤器是解决缓存穿透最有效的方案之一。它可以快速判断数据是否可能存在。
@Service
public class ProductService {
@Autowired
private BloomFilter<String> productBloomFilter;
@Autowired
private ProductRepository productRepository;
@Cacheable(cacheNames = "productCache", key = "#productId",
condition = "@productService.mightExist(#productId)")
public Product getProduct(String productId) {
// 只有布隆过滤器认为可能存在的数据才会查询数据库
return productRepository.findById(productId).orElse(null);
}
public boolean mightExist(String productId) {
// 布隆过滤器快速判断,如果返回false则一定不存在
return productBloomFilter.mightContain(productId);
}
@CachePut(cacheNames = "productCache", key = "#product.id")
public Product saveProduct(Product product) {
// 保存商品时同步更新布隆过滤器
Product savedProduct = productRepository.save(product);
productBloomFilter.put(product.getId());
return savedProduct;
}
}
1.3 空值缓存策略
对于确实不存在的数据,我们可以缓存一个空值,避免重复查询数据库。
@Service
public class UserService {
private static final String NULL_VALUE = "NULL";
@Cacheable(cacheNames = "userCache", key = "#userId")
public User getUserById(String userId) {
User user = userRepository.findById(userId).orElse(null);
// 如果用户不存在,返回一个特殊标记而不是null
return user != null ? user : createNullUser();
}
private User createNullUser() {
User nullUser = new User();
nullUser.setId(NULL_VALUE);
return nullUser;
}
// 在业务层判断是否为空值缓存
public User getValidUser(String userId) {
User user = getUserById(userId);
return NULL_VALUE.equals(user.getId()) ? null : user;
}
}
2. 缓存击穿问题与解决方案
2.1 缓存击穿现象分析
缓存击穿是指热点数据的缓存过期时,大量并发请求同时访问这个数据。
由于缓存中没有数据,所有请求都会打到数据库,可能导致数据库瞬间压力过大。
常见场景:
- 热门商品详情页面
- 明星用户信息
- 热点新闻内容
2.2 互斥锁解决方案
使用分布式锁确保只有一个线程去重建缓存,其他线程等待。
@Service
public class HotDataService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
public Product getHotProduct(String productId) {
String cacheKey = "hot_product:" + productId;
// 先尝试从缓存获取
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 缓存未命中,使用分布式锁
String lockKey = "lock:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待10秒,锁30秒后自动释放
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 双重检查,防止重复查询
product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 查询数据库并更新缓存
product = productRepository.findById(productId).orElse(null);
if (product != null) {
// 设置随机过期时间,防止缓存雪崩
int expireTime = 3600 + new Random().nextInt(600); // 1小时+随机10分钟
redisTemplate.opsForValue().set(cacheKey, product, expireTime, TimeUnit.SECONDS);
}
return product;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
// 获取锁失败,返回空或默认值
return null;
}
}
2.3 逻辑过期解决方案
设置逻辑过期时间,缓存永不过期,通过后台线程异步更新。
@Component
public class LogicalExpireCache {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ThreadPoolExecutor cacheRebuildExecutor;
public Product getProductWithLogicalExpire(String productId) {
String cacheKey = "logical_product:" + productId;
// 获取缓存数据(包含逻辑过期时间)
CacheData<Product> cacheData = (CacheData<Product>) redisTemplate.opsForValue().get(cacheKey);
if (cacheData == null) {
// 缓存不存在,同步查询并设置缓存
return rebuildCacheSync(productId, cacheKey);
}
// 检查逻辑过期时间
if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
// 未过期,直接返回
return cacheData.getData();
}
// 已过期,异步更新缓存,先返回旧数据
cacheRebuildExecutor.submit(() -> rebuildCacheAsync(productId, cacheKey));
return cacheData.getData();
}
private Product rebuildCacheSync(String productId, String cacheKey) {
Product product = productRepository.findById(productId).orElse(null);
if (product != null) {
CacheData<Product> cacheData = new CacheData<>();
cacheData.setData(product);
cacheData.setExpireTime(LocalDateTime.now().plusHours(1)); // 1小时后逻辑过期
redisTemplate.opsForValue().set(cacheKey, cacheData);
}
return product;
}
private void rebuildCacheAsync(String productId, String cacheKey) {
try {
rebuildCacheSync(productId, cacheKey);
} catch (Exception e) {
log.error("异步重建缓存失败: productId={}", productId, e);
}
}
@Data
public static class CacheData<T> {
private T data;
private LocalDateTime expireTime;
}
}
3. 缓存雪崩问题与解决方案
3.1 缓存雪崩场景分析
缓存雪崩是指大量缓存在同一时间过期,导致大量请求直接打到数据库。
这种情况通常发生在系统重启后或者缓存集中过期时。
典型场景:
- 系统重启后缓存全部失效
- 定时任务统一设置的过期时间
- Redis服务器宕机
3.2 随机过期时间策略
通过设置随机过期时间,避免缓存同时失效。
@Service
public class AntiAvalancheService {
@Cacheable(cacheNames = "randomExpireCache", key = "#key")
public Object getCacheWithRandomExpire(String key) {
// Spring缓存注解本身不支持随机过期,需要结合Redis操作
return dataRepository.findByKey(key);
}
@CachePut(cacheNames = "randomExpireCache", key = "#key")
public Object updateCacheWithRandomExpire(String key, Object data) {
// 手动设置随机过期时间
String cacheKey = "randomExpireCache::" + key;
int baseExpire = 3600; // 基础过期时间1小时
int randomExpire = new Random().nextInt(1800); // 随机0-30分钟
redisTemplate.opsForValue().set(cacheKey, data,
baseExpire + randomExpire, TimeUnit.SECONDS);
return data;
}
}
3.3 多级缓存架构
建立多级缓存体系,即使一级缓存失效,还有二级缓存保护。
@Service
public class MultiLevelCacheService {
@Autowired
private CacheManager l1CacheManager; // 本地缓存
@Autowired
private RedisTemplate<String, Object> redisTemplate; // Redis缓存
public Product getProductMultiLevel(String productId) {
// 一级缓存:本地缓存(Caffeine)
Cache l1Cache = l1CacheManager.getCache("productL1Cache");
Product product = l1Cache.get(productId, Product.class);
if (product != null) {
return product;
}
// 二级缓存:Redis缓存
String redisKey = "product:" + productId;
product = (Product) redisTemplate.opsForValue().get(redisKey);
if (product != null) {
// 回写一级缓存
l1Cache.put(productId, product);
return product;
}
// 三级:数据库查询
product = productRepository.findById(productId).orElse(null);
if (product != null) {
// 同时更新两级缓存
l1Cache.put(productId, product);
redisTemplate.opsForValue().set(redisKey, product,
Duration.ofHours(2)); // Redis缓存2小时
}
return product;
}
@CacheEvict(cacheNames = "productL1Cache", key = "#productId")
public void evictProduct(String productId) {
// 同时清除Redis缓存
redisTemplate.delete("product:" + productId);
}
}
4. 电商系统实战案例
4.1 商品详情页缓存策略
电商系统的商品详情页是典型的高并发场景,需要综合应用多种缓存策略。
@Service
public class ProductDetailService {
@Autowired
private BloomFilter<String> productBloomFilter;
@Autowired
private RedissonClient redissonClient;
// 防穿透 + 防击穿的商品详情查询
public ProductDetail getProductDetail(String productId) {
// 1. 布隆过滤器防穿透
if (!productBloomFilter.mightContain(productId)) {
return null; // 商品不存在
}
String cacheKey = "product_detail:" + productId;
// 2. 尝试从缓存获取
ProductDetail detail = (ProductDetail) redisTemplate.opsForValue().get(cacheKey);
if (detail != null) {
return detail;
}
// 3. 缓存未命中,使用分布式锁防击穿
String lockKey = "lock:product_detail:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
// 双重检查
detail = (ProductDetail) redisTemplate.opsForValue().get(cacheKey);
if (detail != null) {
return detail;
}
// 查询数据库
detail = buildProductDetail(productId);
if (detail != null) {
// 4. 设置随机过期时间防雪崩
int expireTime = 7200 + new Random().nextInt(3600); // 2-3小时
redisTemplate.opsForValue().set(cacheKey, detail,
expireTime, TimeUnit.SECONDS);
}
return detail;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return null;
}
private ProductDetail buildProductDetail(String productId) {
// 组装商品详情信息
Product product = productRepository.findById(productId).orElse(null);
if (product == null) {
return null;
}
ProductDetail detail = new ProductDetail();
detail.setProduct(product);
detail.setInventory(inventoryService.getInventory(productId));
detail.setReviews(reviewService.getTopReviews(productId));
detail.setRecommendations(recommendationService.getRecommendations(productId));
return detail;
}
}
4.2 用户会话缓存管理
用户会话信息需要考虑安全性和性能,采用分层缓存策略。
@Service
public class UserSessionService {
// 敏感信息使用短期缓存
@Cacheable(cacheNames = "userSessionCache", key = "#sessionId",
condition = "#sessionId != null")
public UserSession getUserSession(String sessionId) {
return sessionRepository.findBySessionId(sessionId);
}
// 用户基础信息使用长期缓存
@Cacheable(cacheNames = "userBasicCache", key = "#userId")
public UserBasicInfo getUserBasicInfo(String userId) {
return userRepository.findBasicInfoById(userId);
}
@CacheEvict(cacheNames = {"userSessionCache", "userBasicCache"},
key = "#userId")
public void invalidateUserCache(String userId) {
// 用户登出或信息变更时清除相关缓存
log.info("清除用户缓存: {}", userId);
}
// 防止会话固定攻击的缓存更新
@CachePut(cacheNames = "userSessionCache", key = "#newSessionId")
@CacheEvict(cacheNames = "userSessionCache", key = "#oldSessionId")
public UserSession refreshSession(String oldSessionId, String newSessionId, String userId) {
// 生成新的会话信息
UserSession newSession = new UserSession();
newSession.setSessionId(newSessionId);
newSession.setUserId(userId);
newSession.setCreateTime(LocalDateTime.now());
sessionRepository.save(newSession);
sessionRepository.deleteBySessionId(oldSessionId);
return newSession;
}
}
5. 缓存监控与告警
5.1 缓存命中率监控
监控缓存的命中率,及时发现缓存问题。
@Component
public class CacheMetricsCollector {
private final MeterRegistry meterRegistry;
private final Counter cacheHitCounter;
private final Counter cacheMissCounter;
public CacheMetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.cacheHitCounter = Counter.builder("cache.hit")
.description("Cache hit count")
.register(meterRegistry);
this.cacheMissCounter = Counter.builder("cache.miss")
.description("Cache miss count")
.register(meterRegistry);
}
@EventListener
public void handleCacheHitEvent(CacheHitEvent event) {
cacheHitCounter.increment(
Tags.of("cache.name", event.getCacheName()));
}
@EventListener
public void handleCacheMissEvent(CacheMissEvent event) {
cacheMissCounter.increment(
Tags.of("cache.name", event.getCacheName()));
}
// 计算缓存命中率
public double getCacheHitRate(String cacheName) {
double hits = cacheHitCounter.count();
double misses = cacheMissCounter.count();
return hits / (hits + misses);
}
}
5.2 缓存异常告警
当缓存出现异常时,及时告警并降级处理。
@Component
public class CacheExceptionHandler {
@EventListener
public void handleCacheException(CacheErrorEvent event) {
log.error("缓存异常: cache={}, key={}, exception={}",
event.getCacheName(), event.getKey(), event.getException().getMessage());
// 发送告警
alertService.sendAlert("缓存异常",
String.format("缓存 %s 发生异常: %s",
event.getCacheName(), event.getException().getMessage()));
// 记录异常指标
meterRegistry.counter("cache.error",
"cache.name", event.getCacheName()).increment();
}
// 缓存降级处理
@Recover
public Object recoverFromCacheException(Exception ex, String key) {
log.warn("缓存操作失败,执行降级逻辑: key={}", key);
// 直接查询数据库或返回默认值
return fallbackDataService.getFallbackData(key);
}
}
6. 最佳实践总结
6.1 缓存策略选择指南
缓存穿透解决方案选择:
- 数据量大且查询模式固定:使用布隆过滤器
- 数据量小且查询随机性强:使用空值缓存
- 对一致性要求高:布隆过滤器 + 空值缓存组合
缓存击穿解决方案选择:
- 对实时性要求高:使用互斥锁方案
- 对可用性要求高:使用逻辑过期方案
- 并发量特别大:逻辑过期 + 异步更新
缓存雪崩解决方案选择:
- 单机应用:随机过期时间 + 本地缓存
- 分布式应用:多级缓存 + 熔断降级
- 高可用要求:Redis集群 + 多级缓存
6.2 性能优化建议
- 合理设置过期时间:根据数据更新频率设置,避免过长或过短
- 控制缓存大小:定期清理无用缓存,避免内存溢出
- 监控缓存指标:关注命中率、响应时间、错误率等关键指标
- 预热关键缓存:系统启动时预加载热点数据
- 异步更新策略:对于非关键数据,采用异步更新减少响应时间
通过合理应用这些缓存策略,可以有效提升系统性能,保障服务稳定性。
记住,缓存是把双刃剑,既要享受性能提升,也要处理好数据一致性问题。