在 Java 开发中,Redis 作为常用的缓存中间件,可能会面临击穿、穿透、雪崩这三类经典问题。以下是对这三个问题的详细解析及对应的 Java 解决方案:
一、Redis 缓存击穿(Cache Breakdown)
问题描述
- 定义:大量请求同时访问一个过期的热点 key(如秒杀活动中的商品库存),导致请求直接穿透到数据库,引发瞬时高并发压力。
- 核心原因:
- 热点 key 过期时,缓存失效。
- 大量并发请求同时绕过缓存,直达数据库。
Java 解决方案
1. 互斥锁(Mutex Lock)
- 思路:在缓存失效时,通过锁机制确保只有一个线程重建缓存,其他线程等待锁释放后从缓存获取数据。
- 实现步骤:
- 从 Redis 查询数据,若 key 过期或不存在,尝试获取分布式锁(如 Redisson、ZooKeeper 锁)。
- 获得锁的线程查询数据库,更新缓存,并释放锁。
- 其他线程在锁等待期间,休眠或重试查询缓存。
- Java 代码示例(基于 Redisson):
public String getProductInfo(String productId) { String cacheKey = "product:" + productId; String result = redisTemplate.opsForValue().get(cacheKey); if (result == null) { // 缓存失效 RLock lock = redissonClient.getLock("mutex_lock:" + productId); try { lock.lock(); // 加锁 // 二次验证(避免缓存重建期间其他线程重复查询) result = redisTemplate.opsForValue().get(cacheKey); if (result == null) { // 查询数据库 String dbResult = queryFromDatabase(productId); if (dbResult != null) { redisTemplate.opsForValue().set(cacheKey, dbResult, 30, TimeUnit.SECONDS); // 重建缓存 } } } finally { lock.unlock(); // 释放锁 } } return result; }
2. 热点 key 永不过期
- 思路:为热点 key 设置逻辑过期时间(如在 value 中存储过期时间戳),通过异步线程更新缓存,避免主动过期导致的击穿。
- 实现步骤:
- 缓存数据时,在 value 中添加
expireTime
字段。 - 每次访问时,检查
expireTime
,若过期则启动异步线程更新缓存,当前请求仍返回旧数据。
- 缓存数据时,在 value 中添加
- Java 代码示例:
public class CachedData { private String value; private long expireTime; // getter and setter } public String getHotProductInfo(String productId) { String cacheKey = "hot_product:" + productId; CachedData cachedData = redisTemplate.opsForValue().get(cacheKey); if (cachedData == null || System.currentTimeMillis() > cachedData.getExpireTime()) { // 启动异步线程更新缓存(避免阻塞当前请求) CompletableFuture.runAsync(() -> { RLock lock = redissonClient.getLock("hot_product_lock:" + productId); try { lock.lock(); // 二次验证 cachedData = redisTemplate.opsForValue().get(cacheKey); if (cachedData == null || System.currentTimeMillis() > cachedData.getExpireTime()) { String dbResult = queryFromDatabase(productId); cachedData = new CachedData(); cachedData.setValue(dbResult); cachedData.setExpireTime(System.currentTimeMillis() + 30 * 1000); // 逻辑过期时间 redisTemplate.opsForValue().set(cacheKey, cachedData, 60, TimeUnit.SECONDS); // 物理过期时间设为逻辑过期时间的 2 倍 } } finally { lock.unlock(); } }); // 返回旧数据或默认值(若首次查询) return cachedData != null ? cachedData.getValue() : defaultResponse(); } return cachedData.getValue(); }
二、Redis 缓存穿透(Cache Penetration)
问题描述
- 定义:大量请求访问不存在的 key(如恶意攻击、非法参数),导致请求直接穿透缓存,每次都查询数据库,造成数据库压力激增。
- 核心原因:
- 缓存层不存储无效 key,导致所有无效请求直达数据库。
- 攻击方利用不存在的 key 进行批量请求。
Java 解决方案
1. 布隆过滤器(Bloom Filter)
- 思路:在请求进入数据库前,使用布隆过滤器过滤掉不存在的 key,避免无效请求到达数据库。
- 实现步骤:
- 提前将数据库中存在的 key 加载到布隆过滤器中。
- 每次请求先通过布隆过滤器判断 key 是否存在,若不存在则直接返回无效响应。
- Java 代码示例(基于 Google Guava):
// 初始化布隆过滤器(建议使用 Redis 存储布隆过滤器数据,避免内存溢出) private static BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.stringFunnel(Charset.forName("UTF-8")), 1000000, // 预计元素数量 0.01 // 误判率 ); // 服务启动时加载现有 key 到布隆过滤器(示例) @PostConstruct public void loadExistingKeys() { List<String> productIds = productDao.getAllProductIds(); // 从数据库获取所有存在的 productId bloomFilter.putAll(productIds); } public String getProductInfo(String productId) { if (!bloomFilter.mightContain(productId)) { // key 不存在 return "无效的 productId"; } // 正常查询缓存和数据库 String cacheKey = "product:" + productId; String result = redisTemplate.opsForValue().get(cacheKey); if (result == null) { String dbResult = queryFromDatabase(productId); if (dbResult != null) { redisTemplate.opsForValue().set(cacheKey, dbResult, 30, TimeUnit.SECONDS); } else { // 缓存空值(防止重复查询) redisTemplate.opsForValue().set(cacheKey, "", 5, TimeUnit.MINUTES); } return dbResult; } return result; }
2. 缓存空值
- 思路:当数据库查询结果为 null 时,将空值存入缓存(设置较短过期时间),避免后续相同请求穿透到数据库。
- Java 代码示例:
public String getProductInfo(String productId) { String cacheKey = "product:" + productId; String result = redisTemplate.opsForValue().get(cacheKey); if (result == null) { // 缓存未命中 String dbResult = queryFromDatabase(productId); redisTemplate.opsForValue().set(cacheKey, dbResult != null ? dbResult : "", // 空值存为 "" dbResult != null ? 30 : 5, // 存在数据则设正常过期时间,空值设短过期时间(如 5 分钟) TimeUnit.SECONDS); return dbResult; } return result.isEmpty() ? null : result; // 空值返回 null }
三、Redis 缓存雪崩(Cache Avalanche)
问题描述
- 定义:大量缓存 key 同时过期或 Redis 服务宕机,导致大量请求直接涌入数据库,造成数据库负载过高甚至崩溃。
- 核心原因:
- 缓存层大面积失效(如同一批次 key 的过期时间集中设置)。
- Redis 实例故障(如主从切换、集群节点宕机)。
Java 解决方案
1. 过期时间随机化
- 思路:为缓存 key 设置随机过期时间(在固定时间基础上增加随机偏移量),避免大量 key 同时过期。
- Java 代码示例:
public void setProductCache(String productId, String data) { int baseExpireTime = 30 * 60; // 30 分钟 int randomOffset = ThreadLocalRandom.current().nextInt(10 * 60); // 随机偏移 0~10 分钟 int expireTime = baseExpireTime + randomOffset; redisTemplate.opsForValue().set("product:" + productId, data, expireTime, TimeUnit.SECONDS); }
2. 限流与降级
- 思路:
- 限流:通过令牌桶、信号量等机制限制单位时间内进入数据库的请求量(如使用 Hystrix、Resilience4j 或 Spring Cloud Sentinel)。
- 降级:当数据库压力过大时,直接返回默认值或提示信息,保护数据库。
- Java 代码示例(基于 Resilience4j):
// 引入 Resilience4j 依赖 // 添加限流注解 @CircuitBreaker(name = "databaseCircuitBreaker", fallbackMethod = "fallbackGetProductInfo") public String getProductInfo(String productId) { String cacheKey = "product:" + productId; String result = redisTemplate.opsForValue().get(cacheKey); if (result == null) { String dbResult = queryFromDatabase(productId); // 可能触发限流 if (dbResult != null) { redisTemplate.opsForValue().set(cacheKey, dbResult, 30, TimeUnit.SECONDS); } return dbResult; } return result; } // 降级方法 public String fallbackGetProductInfo(String productId, Throwable throwable) { log.error("数据库查询失败,productId: {}, error: {}", productId, throwable.getMessage()); return "服务繁忙,请稍后重试"; // 返回默认值或提示 }
3. Redis 高可用架构
- 思路:搭建 Redis 集群(如 Sentinel 或 Cluster 模式),避免单点故障导致缓存层整体不可用。
- 配置示例(Spring Boot + Redis Cluster):
spring.redis.cluster.nodes=redis://node1:7000,redis://node2:7001,redis://node3:7002 spring.redis.cluster.max-redirects=3
四、总结对比
问题类型 | 核心原因 | 典型解决方案 | Java 关键技术 / 工具 |
---|---|---|---|
击穿 | 单个热点 key 过期 | 互斥锁、热点 key 永不过期 | Redisson、异步线程 |
穿透 | 大量无效 key 请求 | 布隆过滤器、缓存空值 | Guava BloomFilter、Redis 空值缓存 |
雪崩 | 大量 key 同时过期或 Redis 宕机 | 过期时间随机化、限流降级、高可用架构 | Resilience4j、Redis Cluster |
五、最佳实践建议
- 预防为主:
- 对热点数据提前预热缓存,避免突发流量击穿。
- 接口层做参数校验,拦截非法 key(如空值、格式错误)。
- 监控与报警:
- 监控 Redis 内存使用率、缓存命中率、过期 key 数量。
- 监控数据库 QPS、TPS,设置阈值触发报警。
- 综合方案:
- 针对高并发场景,组合使用互斥锁 + 布隆过滤器 + 限流降级,形成多层防护。