缓存击穿、穿透与雪崩
缓存击穿
大量并发请求同时访问一个过期的 key,这些请求会瞬间击穿缓存到达数据库,导致数据库负载。
解决办法
异步定时更新
某一热点数据过期时间为 1 小时,那么每 59 分钟去更新一下该热点数据,就不会存在 key 过期的情况了。
互斥锁
当某个 key 值为空时,在第一个请求访问该 key 时,给这个 key 加锁,然后去数据库读取数据并更新这个 key,更新完之后再解锁。在加锁期间,其余请求都会阻塞。
public String getData(String key) {
String cacheKey = "cache:" + key;
String lockKey = "lock:" + key;
// 1. 查询缓存
String value = redis.get(cacheKey);
if (value != null) {
return value;
}
// 2. 尝试加锁
boolean locked = redis.set(lockKey, "1", "NX", "EX", 10); // 10秒过期
if (locked) {
try {
// 当前线程负责查询 DB
value = db.query(key);
// 回填缓存,设置过期时间
redis.set(cacheKey, value, 60);
} finally {
// 释放锁
redis.del(lockKey);
}
} else {
// 3. 其他线程休眠等待,再次读缓存
try {
Thread.sleep(50);
} catch (InterruptedException e) {}
return getData(key);
}
return value;
}
逻辑过期
缓存值里存一个 逻辑过期时间,即使 key 在 Redis 里没过期,但发现逻辑过期了:
- 返回旧值给用户(不阻塞)
- 后台线程去异步刷新缓存
这种方式避免了用户等待,但需要能容忍短时间的旧数据。
缓存穿透
请求到达缓存时,缓存没数据,然后请求绕过缓存查询数据库,此时数据库也没数据。当这类请求变多时缓存层失去了原本的作用。
缓存穿透的结果和 缓存击穿 一样,大量请求同时到达数据库,区别在于过程不一样:前者有缓存存在,但是没有值,只能再去查询;后者缓存不存在,直接去数据库查询。
解决办法
缓存空值
当第一查询数据库未查询到数据时,就把控制缓存下来,这样后续请求就不会绕过缓存查询数据库了。但是要注意缓存过期时间,如果数据库更新了,缓存再返回空值就不合适了。
此方法有两个弊端:
- 对空值做缓存,会导致 Redis 中缓存了更对的键,占用了更大的空间。此时如果遇到攻击,问题会更严重(大量的攻击请求产生大量空值键)。解决办法是给这类键设置一个较短失效时间
- 持久层和缓存层存在数据不一致的情况。解决办法是使用消息队列异步刷新缓存。
缓存雪崩
大量 key 同时过期或者缓存服务器宕机,导致请求直接到达数据库,造成数据库高负载。
解决办法
不同的过期时间
将不同的 key 的过期时间设置为不同,并定期通过刷新操作更新过期时间,这样就不会出现大量 key 同时过期的情况了。
集群
集群可以有效防止服务不可用的情况。