缓存击穿是怎么回事
你有没有遇到过这种情况:网站平时访问挺快,突然某个热门商品页面卡得不行,刷新几次直接504?其实这背后很可能是缓存“击穿”在作怪。简单说,就是某个热点数据的缓存到期了,但大量请求同时冲向数据库,导致数据库瞬间压力飙升,响应变慢甚至崩溃。
常见的缓存失效策略
为了避免缓存集体失效造成雪崩,通常会采用“随机过期时间”。比如原本设置缓存10分钟过期,实际加个几秒的随机值,让不同缓存错峰失效。
但这只能防雪崩,对单个热点key的击穿还是没辙。比如双十一大促时某个爆款商品详情页,缓存一过期,成千上万的请求全压到数据库,扛不住就挂了。
加锁重建缓存
一个有效的办法是:当发现缓存失效时,只允许一个线程去查数据库并重建缓存,其他请求等着用新的缓存结果。可以用分布式锁实现,比如Redis的SETNX。
String getWithLock(String key) {
String value = redis.get(key);
if (value == null) {
// 尝试获取锁
if (redis.setnx("lock:" + key, "1", 10)) {
try {
value = db.query(key);
redis.setex(key, 300, value); // 重新设置缓存,5分钟
} finally {
redis.del("lock:" + key);
}
} else {
// 没拿到锁,短暂休眠后重试
Thread.sleep(50);
return getWithLock(key);
}
}
return value;
}
永不过期策略
另一种思路是“逻辑过期”,把过期时间存在缓存值里,而不是依赖Redis本身的TTL。每次读取时判断逻辑时间是否过期,如果过期就异步启动更新任务,但返回旧数据给用户,保证可用性。
public class CacheData {
String data;
long expireTime;
}
String getWithLogicalExpire(String key) {
CacheData cacheData = redis.get(key);
if (cacheData == null || System.currentTimeMillis() > cacheData.expireTime) {
// 异步刷新,不影响当前请求返回
asyncRefresh(key);
}
return cacheData != null ? cacheData.data : db.query(key);
}
多级缓存减少穿透风险
除了服务端Redis,还可以在应用本地加一层缓存,比如用Caffeine。这样即使Redis出问题,本地还有热数据顶一阵子。尤其适合访问频率极高的配置类数据。
比如用户登录状态,先查本地缓存,没有再查Redis,还没找到才走数据库。层层拦截,减轻后端压力。
提前预热关键缓存
大促前把热门商品、活动页面的数据提前加载进缓存,避免开抢那一刻集中失效。就像春运抢票前,12306会把热门线路数据提前推送到各级缓存节点。
这种预加载可以结合定时任务,在低峰期自动运行,既保障性能又不增加白天负担。