Redis缓存击穿,缓存穿透,缓存雪崩解决方案(附代码)

简介: Redis缓存击穿,缓存穿透,缓存雪崩解决方案(附代码)

缓存击穿

缓存击穿是指一个Key非常热点,在某些时间点被超高并发地访问,当这个Key在失效的瞬间持续的大并发穿破了缓存(Redis)直接请求到数据库(DB)从而导致数据库出现问题。

解决方案一:使用互斥锁

这种解决方案思路比较简单,就是只让一个线程查询数据库,其它线程等待查询数据库的线程执行完毕并且重新将数据加入缓存,其它线程从缓存获取数据就可以了。


如果是单机系统,可以用synchronized或者lock来处理。分布式系统可以用redis的setnx操作。

单机环境

  • 单机环境中实现的原理大概就是当缓存数据过期后,大量请求进来,只有首个线程可以访问数据库其它线程暂停等主线程查询到数据并释放了锁,其它线程就可以直接在缓存中读取数据了。
public String get(key){
  //从缓存中获取数据
  String value = redis.get(key);
  if(value == null){  //缓存中数据不存在
    if(reenLock.tryLock()){ //获取锁
      //从数据库获取数据
      value=db.get(key);
      //更新缓存数据
      if(value!=null){
        redis.set(key, value, expire_secs);
      }
      //释放锁
      reenLock.unlock();
    }else{  //获取锁失败
      //暂停100ms再重新取获取数据
      Thread.sleep(100);
      value = redis.get(key);
    }
  }
}

分布式环境

在缓存失效的时候先判断拿出来的值为空,不是立即去查库,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的setnx),去set一个mutex key,当操作返回成功时,再进行查库操作并回设缓存。否则,就重试整个get缓存的方法。

public String get(key){
  //从缓存中获取数据
  String value = redis.get(key);
  if(value == null){  //代表缓存值过期
    //设置3分钟的超时,防止删除操作失败的时候,下次缓存过期一直不能查库
    if(redis.setnx(key_mutex,1,3*60) == 1){  //=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;
  } 
}

解决方案二:热点数据永不过期

要值得注意的是,这里说到的永不过期并不是将热点数据存在时间设置为无限制。而是将过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的重建。

b0bf19554fef49a7b1488bc936ca8483.png

从实战看,这种方法对于性能非常友好,唯一不足的就是重建缓存时候,其余线程(非重建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

public String get(Sting key){
  V v = redis.get(key);
  String value = v.getValue(); 
  long timeout = v.getTimeout(); 
  if (v.timeout <= System.currentTimeMillis()){
    // 异步更新后台异常执行
    threadPool.execute(new Runnable(){
      public void run(){
        String keyMutex = "mutex:" + key; 
        if(redis.setnx(keyMutex, "1")){
          //3 min timeout to avoid mutex holder crash
          redis.expire(keyMutex, 3 * 60);
          String dbValue = db.get(key);
          redis.set(key, dbValue);  
           redis.delete(keyMutex);
         }
       }
    });
  }
  return value;
}


解决方案三:定时刷新

后台定义一个job(定时任务)专门主动更新缓存数据,比如一个缓存中的数据过期时间是30分钟,那么job每隔29分钟定时刷新数据(将从数据库中查到的数据更新到缓存中)。

这种方案比较容易理解,但会增加系统复杂度。比较适合那些 key 相对固定。缓存粒度较大的业务,key比较分散的则不太适合,实现起来也比较复杂。


缓存穿透

缓存穿透是指用户恶意的发起大量请求去查询一个缓存(Redis)和数据库(DB)中都没有的数据,出于容错考虑从数据库(DB)查不到数据则不写入缓存(Redis)这将导致每个请求都要到数据库(DB)中查询,失去了缓存的意义,从而导致数据库因压力过大挂掉。

70baa13dbdb2419480ebad615fedf2e0.png


解决方案一:对空数据进行缓存

对空数据进行缓存是一个简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。


这种办法虽然能阻挡大量穿透请求,但是这个空值不具有任何实际业务性,而且如果大量发送获取不存在数据的穿透请求的话(例如恶意攻击),则会浪费缓存空间,如果这种null值过量的话,还会淘汰掉本身缓存存在的数据,这就会使我们的缓存命中率下降。


//伪代码
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;
        }
        //product_list,空值,过期时间
        CacheHelper.Add(cacheKey, cacheValue, cacheTime);
        return cacheValue;
    }
}

解决方案二:布隆过滤器


这种技术在缓存之前再加一层屏障,里面存储目前数据库中存在的所有key。当业务系统有查询请求的时候,首先去BloomFilter中查询该key是否存在。若不存在,则说明数据库中也不存在该数据,因此缓存都不要查了,直接返回null。若存在,则继续执行后续的流程,先前往缓存中查询,缓存中没有的话再前往数据库中的查询。

//伪代码
String get(String key) {
    String value = redis.get(key);     
    if (value  == null) {
        if(!bloomfilter.mightContain(key)){
           //不存在则返回
            return null; 
        }else{
            //可能存在则查数据库
            value = db.get(key); 
            redis.set(key, value); 
        }    
    }
    return value;
}

缓存雪崩

缓存雪崩是指缓存服务器重启或缓存(Redis)中的数据在同一时间内大批量过期,因查询数据量太大引起数据库压力过大甚至宕机。

解决方案一:加锁排队

加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!

注意:在高并发场景下尽可能不用!

//伪代码
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;
    }
}

解决方案二:设置过期时间随机值

避免缓存设置相近的有效期,为有效期增加随机值(1-5分钟)使失效时间均匀分布。这样每一个缓存的过期时间的重复率就会降低,很难引发集体失效的事件。

redis.set(key, value, random);

解决方案三:设置过期标志更新缓存

缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;

缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

//伪代码
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对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。一般通过互斥锁,热点数据永不过期,定时刷新过期时间等方法解决该问题。

  • 缓存穿透 👇

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。一般通过对空数据进行缓存,布隆过滤器等方法解决该问题。

  • 缓存雪崩 👇

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。一般通过加锁排队,设置过期时间随机值等方法解决该问题。


以上解决方案要针对业务系统进行使用,没有最好,只有最合适!



相关文章
|
9月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
4月前
|
缓存 负载均衡 监控
135_负载均衡:Redis缓存 - 提高缓存命中率的配置与最佳实践
在现代大型语言模型(LLM)部署架构中,缓存系统扮演着至关重要的角色。随着LLM应用规模的不断扩大和用户需求的持续增长,如何构建高效、可靠的缓存架构成为系统性能优化的核心挑战。Redis作为业界领先的内存数据库,因其高性能、丰富的数据结构和灵活的配置选项,已成为LLM部署中首选的缓存解决方案。
|
5月前
|
存储 缓存 NoSQL
Redis专题-实战篇二-商户查询缓存
本文介绍了缓存的基本概念、应用场景及实现方式,涵盖Redis缓存设计、缓存更新策略、缓存穿透问题及其解决方案。重点讲解了缓存空对象与布隆过滤器的使用,并通过代码示例演示了商铺查询的缓存优化实践。
268 1
Redis专题-实战篇二-商户查询缓存
|
4月前
|
缓存 运维 监控
Redis 7.0 高性能缓存架构设计与优化
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Redis 7.0高性能缓存架构,探索函数化编程、多层缓存、集群优化与分片消息系统,用代码在二进制星河中谱写极客诗篇。
|
5月前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
缓存 NoSQL 数据库
什么是redis的缓存雪崩与缓存穿透
什么是redis的缓存雪崩与缓存穿透今天来分享一下Redis几道常见的面试题: 如何解决缓存雪崩?如何解决缓存穿透?如何保证缓存与数据库双写时一致的问题?一、缓存雪崩1.1 什么是缓存雪崩?首先我们先来回答一下我们为什么要用缓存(Redis): 1、提高性能能:缓存查询是纯内存访问,而硬盘是磁盘访问,因此缓存查询速度比数据库查询速度快 2、提高并发能力:缓存分组了部分请求,支持更高的并发 现在有个问题,如果我们的缓存挂掉了,这意味着我们的全部请求都跑去数据库了。
1367 0
|
9月前
|
缓存 NoSQL Java
Redis+Caffeine构建高性能二级缓存
大家好,我是摘星。今天为大家带来的是Redis+Caffeine构建高性能二级缓存,废话不多说直接开始~
1270 0
|
9月前
|
消息中间件 缓存 NoSQL
基于Spring Data Redis与RabbitMQ实现字符串缓存和计数功能(数据同步)
总的来说,借助Spring Data Redis和RabbitMQ,我们可以轻松实现字符串缓存和计数的功能。而关键的部分不过是一些"厨房的套路",一旦你掌握了这些套路,那么你就像厨师一样可以准备出一道道饕餮美食了。通过这种方式促进数据处理效率无疑将大大提高我们的生产力。
302 32
|
9月前
|
缓存 NoSQL Java
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
238 5
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
|
11月前
|
缓存 NoSQL Java
Redis应用—8.相关的缓存框架
本文介绍了Ehcache和Guava Cache两个缓存框架及其使用方法,以及如何自定义缓存。主要内容包括:Ehcache缓存框架、Guava Cache缓存框架、自定义缓存。总结:Ehcache适合用作本地缓存或与Redis结合使用,Guava Cache则提供了更灵活的缓存管理和更高的并发性能。自定义缓存可以根据具体需求选择不同的数据结构和引用类型来实现特定的缓存策略。
694 16
Redis应用—8.相关的缓存框架