缓存雪崩


简述缓存雪崩

缓存雪崩,即:缓存中的数据大批量过期,缓存数据同时失效,此时用户发过来的数据请求就全打到数据库上,查询数据多一点,数据库可能直接宕机,从而形成一系列连锁反应,造成整个系统崩溃。

雪崩问题在国外叫做: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 等。


文章作者: Prannt
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Prannt !
评论
  目录