“关于:缓存穿透、缓存击穿、缓存雪崩、热点数据失效”的版本间差异

来自Wikioe
跳到导航 跳到搜索
(建立内容为“category:Redis”的新页面)
 
无编辑摘要
第1行: 第1行:
[[category:Redis]]
[[category:Redis]]
== 关于 ==
项目通常会引入NoSQL技术(即:基于内存的数据库,如redis),来应对大数据量的需求,以规避磁盘读/写速度比较慢的问题而存在的性能弊端;<br/>
但是,引入redis等又有可能出现缓存穿透,缓存击穿,缓存雪崩等问题,并需要开发对这些问题进行考量并作出应对。
== 缓存穿透 ==
缓存穿透:'''key对应的数据在数据源并不存在''',每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。
: 比如:用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
=== 解决方案 ===
有很多种方法可以有效地解决缓存穿透问题:
# 最常见的是采用'''布隆过滤器''':将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
# “缓存value为空的key”:如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然'''把空结果进行缓存''',但它的过期时间会很短,最长不超过五分钟。
#: <syntaxhighlight lang="java">
//伪代码
public object GetProductListNew() {
    int cacheTime = 30;
    String cacheKey = "product_list";
    String cacheValue = CacheHelper.Get(cacheKey);
    if (cacheValue != null) {
        return cacheValue;
    }
    cacheValue = CacheHelper.Get(cacheKey);
    if (cacheValue != null) {
        return cacheValue;
    } else {
        //数据库查询不到,为空
        cacheValue = GetProductListFromDB();
        if (cacheValue == null) {
            //如果发现为空,设置个默认值,也缓存起来
            cacheValue = string.Empty;
        }
        CacheHelper.Add(cacheKey, cacheValue, cacheTime);
        return cacheValue;
    }
}
</syntaxhighlight>
== 缓存击穿 ==
缓存击穿:'''key对应的数据存在,但在redis中过期'''(某个key缓存失效),此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
=== 解决方案 ===
# 使用互斥锁(mutex key):在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
#: <syntaxhighlight lang="java">
public String get(key) {
  String value = redis.get(key);
  if (value == null) {   // 如果缓存过期
      //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
      if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
        value = db.get(key);
        redis.set(key, value, expire_secs);
        redis.del(key_mutex);
      } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
        sleep(50);
        get(key);  //重试
      }
  } else {
      return value;     
  }
}
</syntaxhighlight>
== 缓存雪崩 ==
缓存雪崩:当'''缓存服务器重启或者大量缓存集中在某一个时间段失效'''(大量缓存同时失效),这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
=== 解决方案 ===
# 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写;
#* 加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。甚至可能带来分布式锁的问题,因此,在真正的高并发场景下很少使用!
#: <syntaxhighlight lang="java">
//伪代码
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;
    }
}
</syntaxhighlight>
# 为缓存'''设置不同的失效时间'''(原有的失效时间基础上增加一个随机值),避免集体失效;
# 设置过期标志更新缓存:
#: <syntaxhighlight lang="java">
//伪代码
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;
    }
}
</syntaxhighlight>
* 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
* 缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。
== 热点数据失效 ==
=== 解决方案 ===

2020年10月28日 (三) 03:36的版本


关于

项目通常会引入NoSQL技术(即:基于内存的数据库,如redis),来应对大数据量的需求,以规避磁盘读/写速度比较慢的问题而存在的性能弊端;
但是,引入redis等又有可能出现缓存穿透,缓存击穿,缓存雪崩等问题,并需要开发对这些问题进行考量并作出应对。

缓存穿透

缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。

比如:用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

解决方案

有很多种方法可以有效地解决缓存穿透问题:

  1. 最常见的是采用布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
  2. “缓存value为空的key”:如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
    //伪代码
    public object GetProductListNew() {
        int cacheTime = 30;
        String cacheKey = "product_list";
    
        String cacheValue = CacheHelper.Get(cacheKey);
        if (cacheValue != null) {
            return cacheValue;
        }
    
        cacheValue = CacheHelper.Get(cacheKey);
        if (cacheValue != null) {
            return cacheValue;
        } else {
            //数据库查询不到,为空
            cacheValue = GetProductListFromDB();
            if (cacheValue == null) {
                //如果发现为空,设置个默认值,也缓存起来
                cacheValue = string.Empty;
            }
            CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            return cacheValue;
        }
    }
    

缓存击穿

缓存击穿:key对应的数据存在,但在redis中过期(某个key缓存失效),此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

  1. 使用互斥锁(mutex key):在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
    public String get(key) {
       String value = redis.get(key);
       if (value == null) {   // 如果缓存过期
          //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
          if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
             value = db.get(key);
             redis.set(key, value, expire_secs);
             redis.del(key_mutex);
          } else {   //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
             sleep(50);
             get(key);  //重试
          }
       } else {
          return value;      
       }
    }
    

缓存雪崩

缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效(大量缓存同时失效),这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

解决方案

  1. 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写;
    • 加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。甚至可能带来分布式锁的问题,因此,在真正的高并发场景下很少使用!
    //伪代码
    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. 为缓存设置不同的失效时间(原有的失效时间基础上增加一个随机值),避免集体失效;
  3. 设置过期标志更新缓存:
    //伪代码
    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;
        }
    }
    
  • 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
  • 缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

热点数据失效

解决方案