缓存为何会“击穿”?
在高并发的系统中,缓存就像家门口的快递柜。大多数人取件时都不用跑远路,直接刷码拿走就行。但如果某天快递柜突然清空,所有人只能去几公里外的站点自提,站点瞬间就挤爆了。系统中的缓存击穿就是这个道理:当某个热点数据在缓存中失效的瞬间,大量请求同时涌向数据库,导致数据库压力骤增,甚至可能宕机。
典型的击穿场景
比如双十一大促期间,某款热门商品的详情页被频繁访问。缓存设置为10分钟过期,第10分01秒时缓存刚好失效,下一秒成千上万的请求同时发现缓存没了,全都冲向数据库查原始数据。数据库还没来得及响应,连接池就被耗尽,服务开始变慢甚至崩溃。
常见缓存失效策略
为了避免这种情况,系统通常会采用一些缓存失效策略。最基础的是设置固定过期时间,但这种方式容易造成“集体失效”。更聪明的做法是引入随机过期时间,比如原本设10分钟,实际在9到11分钟之间随机,让缓存不会在同一时刻大批量失效。
long expireTime = 600 + new Random().nextInt(120); // 600秒基础,加0-120秒随机值
redis.setex("product:123", expireTime, data);
击穿的几种防范手段
除了打散过期时间,还可以通过互斥锁机制来控制访问节奏。当缓存失效时,只允许一个线程去数据库加载数据,其他请求要么等待,要么返回旧数据(如果可用)。
String data = redis.get("product:123");
if (data == null) {
if (redis.setnx("lock:product:123", "1", 10)) { // 获取锁
try {
data = db.query("SELECT * FROM products WHERE id=123");
redis.setex("product:123", 600, data);
} finally {
redis.del("lock:product:123"); // 释放锁
}
} else {
// 没拿到锁,短暂休眠后重试或返回默认值
Thread.sleep(50);
data = redis.get("product:123");
}
}
使用逻辑过期避免物理失效
还有一种思路是不真正让缓存过期,而是把过期时间存在缓存值内部。每次读取时判断逻辑时间是否过期,如果过期就异步更新,但当前请求仍返回旧值。这样既保证了可用性,又避免了雪崩。
多级缓存联动
就像家里既有冰箱又有常温柜,系统也可以设置本地缓存(如Caffeine)+ 分布式缓存(如Redis)的组合。即使Redis失效,本地缓存还能撑一段时间,给后端留出反应时间。本地缓存可以设置更短的过期时间,形成梯度防护。
监控与降级同样重要
再好的策略也挡不住意外。线上系统应实时监控缓存命中率和数据库QPS。一旦发现命中率暴跌,自动触发降级策略,比如关闭非核心功能、返回静态兜底页,防止故障扩散。这就像高速堵车时,交管部门临时开放应急车道分流。