"凌晨三点,服务器挂了我才明白什么是缓存击穿!"
这样的场景你是否遇到过?当某个热门微博突然爆炸性传播,对应的缓存键(比如#某明星离婚#)恰好到期失效,瞬间涌入的百万级查询直接穿透缓存层,数据库在十秒内被压垮——这就是经典的缓存击穿现象。今天我们就用真实代码示例,带你彻底解决这个「定时炸弹」。
1. 缓存击穿的底层原理剖析
1.1 缓存三连击的区别
- 穿透:请求不存在的数据(如ID=-1)
- 击穿:热点数据过期瞬间的高并发
- 雪崩:批量缓存失效引发的连锁反应
1.2 危险场景还原
假设某个电商促销的热门商品(SKU=88888)缓存设置了1小时过期:
def get_product_info(sku_id):
cache_key = f"product:{sku_id}"
data = redis.get(cache_key)
if not data:
# 缓存失效时触发数据库查询
db_data = mysql.query("SELECT * FROM products WHERE sku=%s", sku_id)
redis.setex(cache_key, 3600, db_data) # 设置1小时过期
return data
当1小时后缓存失效的瞬间,恰好遇到10万QPS的查询,会导致数据库被打成「植物人状态」。
2. 核心解决方案
2.1 互斥锁方案(分布式锁)
// Java + Redisson示例(技术栈:SpringBoot+Redisson)
public Product getProductWithLock(String skuId) {
String cacheKey = "product:" + skuId;
Product product = redisService.get(cacheKey);
if (product == null) {
RLock lock = redissonClient.getLock("lock:" + skuId);
try {
// 尝试获取锁,最多等待200ms,锁持有5秒
if (lock.tryLock(200, 5000, TimeUnit.MILLISECONDS)) {
// 双重检查避免重复查询
product = redisService.get(cacheKey);
if (product == null) {
product = dbService.queryProduct(skuId);
redisService.setEx(cacheKey, 3600, product);
}
} else {
// 未获取锁时休眠50ms后重试
Thread.sleep(50);
return getProductWithLock(skuId);
}
} finally {
lock.unlock();
}
}
return product;
}
应用场景:金融交易、库存扣减等高一致性要求的系统
优势:数据强一致性保障
缺陷:锁竞争可能增加延迟,需设置合理的重试策略
2.2 逻辑过期方案
# Python + Redis示例(技术栈:Flask+redis-py)
def get_product_with_logic_expire(sku_id):
cache_key = f"product:{sku_id}"
data = redis.get(cache_key)
if not data:
return load_and_set_data(sku_id)
item = json.loads(data)
# 检查逻辑过期时间
if time.time() > item['expire']:
# 异步更新缓存
threading.Thread(target=load_and_set_data, args=(sku_id,)).start()
return item['data']
def load_and_set_data(sku_id):
# 获取分布式锁
lock_key = f"lock:{sku_id}"
with redis.lock(lock_key, timeout=3):
# 防止重复更新
db_data = mysql.query_product(sku_id)
new_data = {
'data': db_data,
'expire': time.time() + 3600 # 设置下一轮过期时间
}
redis.setex(f"product:{sku_id}", 7200, json.dumps(new_data))
适用场景:资讯类、商品详情页等允许短暂延迟
技巧:物理过期时间设置为逻辑的两倍,保证异步更新窗口
(因篇幅限制,此处仅展示两个完整示例,其他方案将继续按相同格式展开)
3. 进阶混合方案
3.1 多级缓存架构
// Java + Caffeine本地缓存示例
@Cacheable(value = "productCache", key = "#skuId")
public Product getProductMultiLevel(String skuId) {
// 第一层:本地缓存(Caffeine)
// 第二层:Redis集群
// 第三层:数据库
return fallbackQuery(skuId);
}
// 降级方法(数据库查询+限流)
@SentinelResource(
value = "fallbackQuery",
fallback = "queryFallback",
blockHandler = "queryBlockHandler"
)
private Product fallbackQuery(String skuId) {
return dbService.query(skuId);
}
组合策略:本地缓存设置随机过期时间(300-400秒),配合Redis的固定过期
4. 场景选择指南
方案 | QPS承受力 | 数据延迟 | 实现复杂度 | 适用场景 |
---|---|---|---|---|
互斥锁 | 中等 | 低 | 高 | 商品秒杀 |
逻辑过期 | 高 | 中 | 中 | 新闻资讯 |
限流降级 | 超高 | 高 | 低 | 突发流量 |
预加载更新 | 高 | 低 | 高 | 定时促销活动 |
5. 注意事项
- 缓存更新时间:根据业务峰值设定过期时间偏移(如避开整点)
- 热点发现策略:通过监控Redis的命中率自动识别热key
- 熔断机制:当数据库访问超时时自动熔断,返回兜底数据
- 压力测试:使用jmeter模拟百万级并发测试方案有效性
6. 总结
解决缓存击穿就像给数据高速公路加装应急车道——互斥锁是交警人工疏导,逻辑过期是智能交通信号灯,限流降级则是高速公路入口的流量控制。没有万能方案,只有最适合当前业务阶段的组合策略。记住:好的架构师永远留着Plan B!