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

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,倚天版 1GB 1个月
简介: 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)带来很大压力。一般通过加锁排队,设置过期时间随机值等方法解决该问题。


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



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1天前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
这篇文章介绍了如何在SpringBoot项目中整合Redis,并探讨了缓存穿透、缓存雪崩和缓存击穿的问题以及解决方法。文章还提供了解决缓存击穿问题的加锁示例代码,包括存在问题和问题解决后的版本,并指出了本地锁在分布式情况下的局限性,引出了分布式锁的概念。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
|
1天前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
这篇文章是关于如何在SpringBoot应用中整合Redis并处理分布式场景下的缓存问题,包括缓存穿透、缓存雪崩和缓存击穿。文章详细讨论了在分布式情况下如何添加分布式锁来解决缓存击穿问题,提供了加锁和解锁的实现过程,并展示了使用JMeter进行压力测试来验证锁机制有效性的方法。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
|
5天前
|
存储 缓存 NoSQL
基于SpringBoot+Redis解决缓存与数据库一致性、缓存穿透、缓存雪崩、缓存击穿问题
这篇文章讨论了在使用SpringBoot和Redis时如何解决缓存与数据库一致性问题、缓存穿透、缓存雪崩和缓存击穿问题,并提供了相应的解决策略和示例代码。
16 0
|
缓存 NoSQL 数据库
什么是redis的缓存雪崩与缓存穿透
什么是redis的缓存雪崩与缓存穿透今天来分享一下Redis几道常见的面试题: 如何解决缓存雪崩?如何解决缓存穿透?如何保证缓存与数据库双写时一致的问题?一、缓存雪崩1.1 什么是缓存雪崩?首先我们先来回答一下我们为什么要用缓存(Redis): 1、提高性能能:缓存查询是纯内存访问,而硬盘是磁盘访问,因此缓存查询速度比数据库查询速度快 2、提高并发能力:缓存分组了部分请求,支持更高的并发 现在有个问题,如果我们的缓存挂掉了,这意味着我们的全部请求都跑去数据库了。
1283 0
|
4天前
|
NoSQL 数据可视化 Redis
Mac安装Redis
Mac安装Redis
15 3
|
5天前
|
NoSQL Ubuntu 安全
在Ubuntu 18.04上安装和保护Redis的方法
在Ubuntu 18.04上安装和保护Redis的方法
14 0
|
1天前
|
NoSQL Redis 数据安全/隐私保护
Redis6入门到实战------ 二、Redis安装
这篇文章详细介绍了Redis 6的安装过程,包括下载、解压、编译、安装、配置以及启动Redis服务器的步骤。还涵盖了如何设置Redis以在后台运行,如何为Redis设置密码保护,以及如何配置Redis服务以实现开机自启动。
Redis6入门到实战------ 二、Redis安装
|
1天前
|
NoSQL 数据可视化 Linux
2022 年超详细步骤讲解 CentOS 7 安装Redis 。解决Redis Desktop Manager 图形化工具连接失败解决 ;connection failed处理。开机自启Redis
这篇文章提供了在CentOS 7上安装Redis的详细步骤,包括上传Redis安装包、解压安装、编译、安装、备份配置文件、修改配置以支持后台运行和设置密码、启动Redis服务、使用客户端连接Redis、关闭Redis服务、解决Redis Desktop Manager图形化工具连接失败的问题、设置Redis开机自启动,以及Redis服务的启动和停止命令。
2022 年超详细步骤讲解 CentOS 7 安装Redis 。解决Redis Desktop Manager 图形化工具连接失败解决 ;connection failed处理。开机自启Redis
|
5天前
|
存储 NoSQL Java
使用redis进行手机验证码的验证、每天只能发送三次验证码 (redis安装在虚拟机linux系统中)
该博客文章展示了如何在Linux虚拟机上使用Redis和Jedis客户端实现手机验证码的验证功能,包括验证码的生成、存储、验证以及限制每天发送次数的逻辑,并提供了测试结果截图。
使用redis进行手机验证码的验证、每天只能发送三次验证码 (redis安装在虚拟机linux系统中)
|
5天前
|
NoSQL Linux 网络安全
Linux系统安装Redis
该博客文章详细介绍了在Linux系统中安装Redis的步骤,包括下载、编译、配置、启动Redis服务以及使用客户端访问Redis数据库的过程。
Linux系统安装Redis