简述缓存雪崩
缓存雪崩,即:缓存中的数据大批量过期,缓存数据同时失效,此时用户发过来的数据请求就全打到数据库上,查询数据多一点,数据库可能直接宕机,从而形成一系列连锁反应,造成整个系统崩溃。
雪崩问题在国外叫做:stampeding herd(奔逃的野牛),指的是cache crash后,流量会像奔逃的野牛一样,打向后端。
以下分别从MySQL的角度和Redis的角度来分析这个问题:
从MySQL的角度出发
无论是对很多键(key)的并发查询(缓存雪崩),还是一次性对同一个键的很多次并发查询(缓存击穿,下篇文章),MySQL的解决方法都是简单粗暴:强行减少请求量。
如何减少请求量呢?答案是:Synchronized,同步锁。如下左图所示,没有加锁时,线程可以并发读取数据库;右图中加锁了,线程只能同步访问数据库,等一个线程执行完并释放锁,其他线程才可以竞争这把锁。这样一来,数据库的压力瞬间就降下来了。
这个解决方法带来的问题也很明显:用户体验卡顿。通过牺牲用户体验来保证数据库不宕机。
// 模拟加锁排队
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
String lockKey = cacheKey;
String cacheValue = CacheHelper.get(cacheKey);
if (cacheValue != null) {
return cacheValue; // 返回缓存中的数据
} else { // 直接访问数据库
synchronized(lockKey) { // 同步锁
cacheValue = CacheHelper.get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
// sql查询数据
cacheValue = GetProductListFromDB();
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
}
}
return cacheValue;
}
}
相对于缓存击穿的情形,对缓存雪崩采取这种策略,效果并不会这么给力。原因也很简单,此时很多客户端的访问请求,不单单是基于一个键,并发量的减少不会像缓存击穿这么明显。
2.从Redis的角度出发
- 设置热点数据不过期
- 设置随机过期时间,避免缓存在同一时间大批失效。换句话,就是让缓存失效时间均匀一点。
// 模拟设置随机过期时间
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
// 缓存标记:记录缓存数据是否过期
String cacheSign = cacheKey + "_sign";
String sign = CacheHelper.Get(cacheSign);
// 获取缓存值
String cacheValue = CacheHelper.Get(cacheKey);
if (sign != null) {
return cacheValue; // 未过期,直接返回
} else {
CacheHelper.Add(cacheSign, "1", cacheTime);
ThreadPool.QueueUserWorkItem((arg) -> {
// sql查询数据
cacheValue = GetProductListFromDB();
// 日期设为缓存时间的2倍,用于脏读
CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);
});
return cacheValue;
}
}
如果我们一开始就没缓存怎么办?
在项目发布伊始,数据库中存满了数据,缓存却是空的。如果此时系统上线,请求又多,那就是妥妥的雪崩。甚至可以说,雪崩是这种情况下的必然事件。
那么这种情况应该怎么处理呢?这里得引入一个名词:灰度发布。
灰度发布
首先灰这个词是怎么来的呢?当然是由黑和白混合出来的,互联网一般会采用新系统完全上线或者新系统没上线定义为黑和白。所以新产品在处于没有完全上线的状态,就被定义为灰了。我们经常听说的A/B测试就是一种灰度发布方式,即一部分用户继续用老系统A,另一部分用户用新系统B,如果用户对新系统意见不大,甚至反馈比A好,那就逐步扩大范围,慢慢把所有用户都迁移到B上来。
如果采用灰度发布进行缓存预热,一开始流量小,数据库能hold住,缓存也可以多出很多数据,渐渐增加流量,缓存也不为空,就轻轻松松解决这个问题了。
缓存雪崩处理原则
面试官:如果某个查询逻辑确实复杂,数据库回应很慢,Redis对这条查询又没缓存,这种情况引起雪崩,我们怎么处理?
题目解析:用户A发了个非常复杂的查询请求,后端数据库收到查询后,开始执行查询,由于查询可能涉及多表联查,花的时间不少,还在查询过程中, 此时用户B,C,D,E,F…..发了同样复杂的请求过来,由于缓存此时为空,请求毫无疑问,就打到了数据库上,要是服务器性能差点,数据库就雪崩了。此时咱们外层看着好像Mysql收到的请求好像也不多啊…
遇到这种情况,一定具体分析业务大体需求了。
- 如果注重时效性,那就得一开始就对这些复杂查询做缓存。
- 不注重,那就上锁,加锁,超时的请求返回错误信息等等,减少数据库压力。
预防和解决缓存雪崩问题
一、保证缓存层服务的高可用
如果缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 Redis Sentinel 和 Redis Cluster 都实现了高可用。
部署方式一:双机房部署,一套Redis Cluster,部分机器在一个机房,另一部分机器在另外一个机房。
部署方式二:双机房部署,两套Redis Cluster,两套Redis Cluster之间做一个数据同步。
二、设置缓存不过期
Redis中保存的 key 永不失效,这样就不会出现大量缓存同时失效的问题,但是随之而来的就是Redis 需要更多的存储空间。
三、优化缓存过期时间
设计缓存时,为每一个 key 选择合适的过期时间,避免大量的 key 在同一时刻同时失效,造成缓存雪崩。
四、熔断、降级组件
无论是缓存层还是存储层都会有出错的概率,可以将它们视为资源。作为并发量较大的分布式系统,假如有一个资源不可用,可能会造成所有线程在获取这个资源时异常,造成整个系统不可用。降级在高并发系统中是非常正常的,比如推荐服务中,如果个性化推荐服务不可用,可以降级补充热点数据,不至于造成整个推荐服务不可用。常见的限流降级组件如 Hystrix、Sentinel 等。