zoukankan      html  css  js  c++  java
  • 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.不同业务场景需要设置的缓存层都不同,需要根据不同情况来处理。

      

  • 相关阅读:
    Network in Network
    cord-in-a-box 2.0 安装指南
    L2 约束的最小二乘学习法
    点估计
    递归简介
    向量的L2范数求导
    优雅的线性代数系列三
    ansible批量部署nginx
    ansible批量部署mysql
    ansible批量部署tomcat
  • 原文地址:https://www.cnblogs.com/shadoll/p/14419668.html
Copyright © 2011-2022 走看看