在高并发系统中,Redis作为核心缓存组件,其稳定性直接决定了系统的性能与可靠性。本文将围绕缓存穿透、缓存击穿、缓存雪崩三大经典问题,从概念解析到Java实现,提供完整的解决方案与代码示例。
一、缓存穿透:恶意请求的防御战
1.1 概念解析
缓存穿透指客户端请求的数据既不在缓存中,也不在数据库中(如恶意构造的无效ID),导致每次请求都会穿透缓存直达数据库。攻击者可通过高频发送此类请求耗尽数据库资源。
典型场景:
- 恶意用户扫描全量ID进行查询
- 业务误传参数生成非法key(如`user:-1`)
1.2 解决方案与Java实现
方案一:布隆过滤器拦截
使用Guava布隆过滤器过滤非法请求:
--java
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterDemo {
private static final int EXPECTED_INSERTIONS = 1_000_000;
private static final double FPP = 0.01; // 误判率1%
public static void main(String args) {
// 初始化布隆过滤器
BloomFilter<Integer> filter = BloomFilter.create(
Funnels.integerFunnel(),
EXPECTED_INSERTIONS,
FPP
);
// 写入合法ID
filter.put(1001);
filter.put(2002);
// 查询校验
if (!filter.mightContain(1001)) {
System.out.println("非法请求拦截成功");
}
}
}
方案二:缓存空值策略
对无效查询结果进行缓存:
--java
public class CacheNullValueDemo {
private Jedis jedis = new Jedis("localhost", 6379);
public String getData(String key) {
String data = jedis.get(key);
if (data == null) {
data = dbQuery(key); // 模拟数据库查询
if (data == null) {
// 缓存空值,TTL设置为5分钟
jedis.setex(key, 300, "NULL_VALUE");
} else {
// 正常缓存数据,TTL 1小时
jedis.setex(key, 3600, data);
}
}
return "NULL_VALUE".equals(data) ? "{}" : data;
}
private String dbQuery(String key) {
// 模拟数据库查询逻辑
return null;
}
}
二、缓存击穿:热点数据的生死时速
2.1 现象描述
当某个超高频访问的热点Key(如明星八卦新闻)在缓存中过期时,瞬时大量请求将直接打到数据库,形成"原子弹式"冲击。
关键特征:
- 单一Key请求量激增
- 数据库连接池迅速耗尽
- 响应时间指数级上升
2.2 解决方案与Java实现
方案一:分布式锁+异步重建
使用Redisson实现分布式锁:
--java
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;
public class CacheBreakdownDemo {
private static RedissonClient redisson;
static {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
redisson = Redisson.create(config);
}
public String getHotData(String key) {
String data = jedis.get(key);
if (data == null) {
RLock lock = redisson.getLock("lock:" + key);
if (lock.tryLock(10, TimeUnit.SECONDS)) {
try {
// 双重检查
data = jedis.get(key);
if (data == null) {
data = dbLoad(key); // 从数据库加载
jedis.setex(key, 3600, data);
}
} finally {
lock.unlock();
}
} else {
// 降级处理
return getDataFromBackup();
}
}
return data;
}
private String dbLoad(String key) {
// 模拟数据库加载
return "hot_data";
}
}
方案二:永不过期设计
--java
public class EternalCacheDemo {
public void initEternalCache() {
// 设置超长过期时间(10天)
jedis.setex("critical_key", 86400000, "eternal_value");
// 后台线程定时更新
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
String newData = dbQuery("critical_key");
jedis.set("critical_key", newData);
}, 0, 5, TimeUnit.MINUTES);
}
}
三、缓存雪崩:系统性风险的连环爆
3.1 灾难场景还原
当大量缓存Key在同一时间段集中过期(如凌晨批量更新),或Redis集群大规模宕机时,所有请求将直接涌向数据库。
风险诱因:
- 开发人员统一设置相同过期时间
- 运维误操作导致缓存服务宕机
- 大规模数据续期失败
3.2 解决方案与Java实现
方案一:随机过期时间偏移
--java
public class RandomTTLDemo {
public void setWithRandomTTL(String key, String value) {
int baseTTL = 3600; // 基础1小时
int randomOffset = new Random().nextInt(300); // 随机0-300秒
int ttl = baseTTL + randomOffset;
jedis.setex(key, ttl, value);
}
}
方案二:熔断降级机制
使用Sentinel实现熔断:
--java
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
public class CircuitBreakerDemo {
public String queryWithSentinel(String key) {
try (Entry entry = SphU.entry("db_query")) {
return jedis.get(key);
} catch (BlockException e) {
// 触发降级逻辑
return getDataFromLocalCache(key);
}
}
private String getDataFromLocalCache(String key) {
// 从Caffeine本地缓存获取
return localCache.getIfPresent(key);
}
}
方案三:多级缓存架构
--java
public class MultiLevelCache {
private CaffeineCache localCache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
public String get(String key) {
// 先查本地缓存
ValueWrapper wrapper = localCache.getIfPresent(key);
if (wrapper != null) {
return (String) wrapper.get();
}
// 本地未命中则查Redis
String redisData = jedis.get(key);
if (redisData != null) {
localCache.put(key, redisData);
return redisData;
}
// 最后查数据库
String dbData = db.load(key);
if (dbData != null) {
jedis.setex(key, 3600, dbData);
localCache.put(key, dbData);
}
return dbData;
}
}
四、结语
面对缓存穿透、击穿、雪崩三大难题,核心在于构建预防-控制-恢复的完整体系。实际应用中需注意:
1. 布隆过滤器需定期重建
2. 分布式锁要考虑集群脑裂问题
3. 熔断策略需配合降级预案
通过合理的架构设计与Java技术栈的深度结合,可有效提升系统在极端场景下的鲁棒性。记住:优秀的缓存策略不是追求零故障,而是建立快速恢复的能力,这正是高可用系统的精髓所在。