【JAVA架构师成长之路】【Redis】第13集:Redis缓存击穿原理、规避、解决方案
30分钟自学教程:Redis缓存击穿原理与解决方案
目标
- 理解缓存击穿的定义及核心原因。
- 掌握互斥锁、逻辑过期时间等预防技术。
- 能够通过代码实现高并发场景下的缓存保护。
- 学会熔断降级、热点探测等应急方案。
教程内容
0~2分钟:缓存击穿的定义与典型场景
- 定义:某个热点Key突然过期,导致瞬时高并发请求直接穿透缓存访问数据库,引发数据库压力激增。
- 与雪崩、穿透的区别:
- 雪崩:大量Key同时失效。
- 穿透:查询不存在的数据。
- 击穿:单个热点Key失效引发并发问题。
- 典型场景:
- 秒杀商品Key过期时,数万用户同时刷新页面。
- 新闻热点事件缓存到期,大量用户请求涌入。
2~5分钟:代码模拟击穿场景(Java示例)
// 未做并发控制的查询方法(模拟击穿问题)
public Product getProduct(String id) {
String key = "product:" + id;
Product product = redisTemplate.opsForValue().get(key);
if (product == null) {
// 假设此时有1000个并发请求同时进入此逻辑
product = productService.loadFromDB(id);
redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS);
}
return product;
}
问题复现:
- 使用JMeter或Postman模拟100个并发请求访问该接口,观察数据库查询次数是否为1次(理想)或100次(实际击穿现象)。
5~12分钟:解决方案1——互斥锁(分布式锁)
- 核心思想:缓存失效时,仅允许一个线程重建数据,其他线程阻塞等待或重试。
- 代码实现(Redisson分布式锁):
public Product getProductWithLock(String id) {
String key = "product:" + id;
Product product = redisTemplate.opsForValue().get(key);
if (product == null) {
RLock lock = redissonClient.getLock("product_lock:" + id);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { // 尝试获取锁,最多等待3秒
// 双重检查:其他线程可能已更新缓存
product = redisTemplate.opsForValue().get(key);
if (product == null) {
product = productService.loadFromDB(id);
redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS);
}
} else {
Thread.sleep(50); // 未获取锁,短暂等待后重试
return getProductWithLock(id);
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
return product;
}
- 关键点:
- 锁粒度需精确到单个Key,避免全局锁性能问题。
- 必须设置锁超时时间,防止死锁。
12~20分钟:解决方案2——逻辑过期时间
- 原理:缓存永不过期,但业务层判断数据是否需更新(牺牲一定一致性)。
- 代码实现(封装逻辑过期时间):
// 数据包装类,包含逻辑过期时间
public class ProductWrapper {
private Product product;
private long expireTime; // 逻辑过期时间戳
// 构造方法、Getter/Setter
}
public Product getProductWithLogicExpire(String id) {
String key = "product:" + id;
ProductWrapper wrapper = redisTemplate.opsForValue().get(key);
if (wrapper == null) {
// 首次加载数据
Product product = productService.loadFromDB(id);
wrapper = new ProductWrapper(product, System.currentTimeMillis() + 3600 * 1000);
redisTemplate.opsForValue().set(key, wrapper);
return product;
} else if (wrapper.getExpireTime() < System.currentTimeMillis()) {
// 逻辑过期,异步更新缓存
CompletableFuture.runAsync(() -> {
Product newProduct = productService.loadFromDB(id);
redisTemplate.opsForValue().set(key,
new ProductWrapper(newProduct, System.currentTimeMillis() + 3600 * 1000)
);
});
}
return wrapper.getProduct(); // 返回旧数据,保证可用性
}
- 适用场景:
- 读多写少的热点数据(如商品详情、新闻内容)。
- 允许短暂的数据不一致。
20~25分钟:解决方案3——热点Key探测与自动续期
- 原理:监控高频访问的Key,提前续期或标记为永久有效。
- 代码实现(简单监控逻辑):
// 热点Key监控器(伪代码)
public class HotKeyMonitor {
private ConcurrentHashMap<String, AtomicInteger> accessCount = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 60000) // 每分钟扫描一次
public void scanHotKeys() {
accessCount.forEach((key, count) -> {
if (count.get() > 1000) { // 访问量阈值
redisTemplate.expire(key, 2, TimeUnit.HOURS); // 自动续期
count.set(0); // 重置计数器
}
});
}
public void recordAccess(String key) {
accessCount.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet();
}
}
// 在查询方法中记录访问
public Product getProduct(String id) {
String key = "product:" + id;
hotKeyMonitor.recordAccess(key);
// ...其他逻辑
}
25~28分钟:应急处理方案
- 熔断降级(Sentinel示例):
@SentinelResource(value = "getProduct", blockHandler = "handleBlock")
public Product getProduct(String id) {
// 正常业务逻辑...
}
// 熔断后返回默认数据
public Product handleBlock(String id, BlockException ex) {
return new Product("默认商品", 0.0);
}
- 缓存预热:
// 定时预热即将过期的热点Key
@Scheduled(cron = "0 0/5 * * * ?") // 每5分钟执行一次
public void preloadHotData() {
List<String> hotKeys = hotKeyMonitor.getHotKeys();
hotKeys.forEach(key -> {
Long ttl = redisTemplate.getExpire(key);
if (ttl != null && ttl < 60) { // 剩余时间小于60秒
String id = key.split(":")[1];
Product product = productService.loadFromDB(id);
redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS);
}
});
}
28~30分钟:总结与优化方向
- 核心原则:分散并发压力、异步更新、监控兜底。
- 高级优化:
- 结合Redis Cluster实现高可用。
- 使用本地缓存(如Caffeine)作为一级缓存。
练习与拓展
练习
- 实现基于Redisson的分布式锁,并通过JMeter验证并发控制效果。
- 修改逻辑过期时间代码,支持动态调整过期阈值(如30分钟~2小时随机)。
推荐拓展
- 研究RedLock算法及其在分布式锁中的应用。
- 学习Redis的持久化机制(RDB/AOF)与数据恢复。
- 探索开源框架JetCache对缓存击穿的自动化处理方案。