Redis做缓存时,出现的缓存击穿、缓存穿透、缓存雪崩等问题及解决方案

一.前言 

  Redis可以用于做缓存层框架,当请求数据时,先从缓存中读取,可以减少SQL数据库读取的压力。本文会讲解一下使用缓存时会出现的问题,如缓存击穿、缓存穿透、缓存雪崩。

二.缓存击穿

  缓存中没有但数据库有的数据(一般是缓存时间到期),这时候由于并发用户特别多,同时读缓存没读到数据,就同时去读数据库,对数据库CPU和内存造成巨大压力,严重时会造成数据库宕机,从而形成一系列的连锁反应,造成系统崩溃。

  解决方案:

  1.给缓存设置永不过期。

  但这样缺点很明显,需要我们手动更新缓存,缓存数据的更新会有延迟。

  2.加互斥锁。

  通俗地讲,就是当有一万个用户访问这一条数据,但是只有一个用户才能访问数据库去拿到这个数据,等这个用户拿到数据后,再把这条数据存到缓存中。剩下的用户要等待到这条数据加入缓存后,才去访问缓存获取数据。

// 定义一个标识确保线程同步
private static readonly object locker = new object();
[Route("get")]
[HttpGet]
public string Get(string key)
{
    const int cacheTime = 30;

    // 从Redis里面取数据
    string cacheValue = _redis.StringGet(key);
    if (cacheValue != null)
    {
        return cacheValue;
    }
    else
    {
        lock (locker)
        {
            cacheValue = _redis.StringGet(key);
            if (cacheValue != null)
            {
                return cacheValue;
            }
            else
            {
                //这里一般是 sql查询数据。  
                cacheValue = GetValueFromDB();      
                //重新给key设置缓存
                _redis.StringSet(key, cacheValue, cacheTime);
            }
        }
        return cacheValue;
    }
}

  第一次访问的线程进到相关位置后会给locker对象加锁,等缓存重新设置后才解锁。其它线程运行到相关位置就只能挂起,等待locker对象解锁才能继续运行。

  加互斥锁只能够减少数据库的压力,如果是高并发的情况下,加了互斥锁,在缓存重新创建之前,1000条请求会有999条被挂起,会造成用户等待超时。

  3.给每个缓存数据添加一个缓存标记,记录缓存是否失效,如果失效的话,就更新缓存数据。

public string Get(string key)
{
    const int cacheTime = 30;
    //缓存标记。
    string cacheSign = key + "_sign";

    string sign = _redis.StringGet(cacheSign);
    //获取缓存值
    string cacheValue = _redis.StringGet(key);
    if (sign != null)
    {
        return cacheValue; //未过期,直接返回。
    }
    else
    {
        _redis.StringSet(cacheSign, "1", cacheTime);
        ThreadPool.QueueUserWorkItem((arg) =>
        {
            //这里一般是 sql查询数据。
            cacheValue = GetValueFromDB(); 
            //日期设缓存时间的2倍
            _redis.StringSet(key, cacheValue, cacheTime);
        });

        return cacheValue;
    }
}

  缓存数据的生存时间要比它对应的缓存标记的生存时间要长。这样,当缓存标记过期了,还是可以从缓存数据中读取旧的缓存数据,直到另外开启的线程更新缓存,才能得到新的缓存数据。这个方案可以在一定程度上提高系统的吞吐量。

三.缓存穿透

  数据库中没有的数据,缓存当然也不会有,而用户不断发起请求去查询这个数据,这样就会绕过缓存,每次都会直接去数据库进行查询。绕过缓存去查数据库,这就是我们常说的缓存命中率的问题。

  解决方案是,如果数据库不存在该数据,在缓存中也设置一条空数据,这样第二次访问的时候,就不会绕过缓存,去读取数据库,把压力转到缓存上。

[Route("get")]
[HttpGet]
public string Get(string key)
{
    const int cacheTime = 30;

    //获取缓存值
    string cacheValue = _redis.StringGet(key);
    if (cacheValue != null)
    {
        return cacheValue;
    }
    else
    {
        cacheValue = GetValueFromDB(); //数据库查询不到,为空。

        if (cacheValue == null)
        {
            cacheValue = string.Empty; //如果发现为空,设置个默认值,也缓存起来。                
        }
        _redis.StringSet(key, cacheValue, cacheTime);

        return cacheValue;
    }
}

四.缓存雪崩

  指缓存中的大量数据在同一个时间点到期,而在这个时间点,恰好有大批量的访问,造成数据库压力过大。

  和缓存击穿不同的是,缓存击穿指的是同一条数据被高并发查询,而缓存雪崩指的是大批量缓存数据同时失效,很多数据查不到才同时去查询数据库。

  解决方案,缓存数据的生存时间设置要随机,防止同一个时间点出现大量缓存数据失效的情况发生。

五.总结

  1.用Redis做缓存,遇到的问题和其它各类缓存机制都大同小异,遇到的问题也大致为上述问题,需要提前预防。

  2.做缓存的目的是放在数据库压力过大。

  3.不同业务场景需要设置的缓存层都不同,需要根据不同情况来处理。

  

posted @ 2021-03-05 17:18  shine声  阅读(213)  评论(0编辑  收藏  举报