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

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 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 中间件
【后端面经】【缓存】36|Redis 单线程:为什么 Redis 用单线程而 Memcached 用多线程?epoll、poll和select + Reactor模式
【5月更文挑战第18天】`epoll`、`poll`和`select`是Linux下多路复用IO的三种方式。`select`需要主动调用检查文件描述符,而`epoll`能实现回调,即使不调用`epoll_wait`也能处理就绪事件。`poll`与`select`类似,但支持更多文件描述符。面试时,重点讲解`epoll`的高效性和`Reactor`模式,该模式包括一个分发器和多个处理器,用于处理连接和读写事件。Redis采用单线程模型结合`epoll`的Reactor模式,确保高性能。在Redis 6.0后引入多线程,但基本原理保持不变。
9 2
|
2天前
|
缓存 NoSQL Redis
【后端面经】【缓存】36|Redis 单线程:为什么 Redis 用单线程而 Memcached 用多线程?--epoll调用和中断
【5月更文挑战第18天】`epoll`包含红黑树和就绪列表,用于高效管理文件描述符。关键系统调用有3个:`epoll_create()`创建epoll结构,`epoll_ctl()`添加/删除/修改文件描述符,`epoll_wait()`获取就绪文件描述符。`epoll_wait()`可设置超时时间(-1阻塞,0立即返回,正数等待指定时间)。当文件描述符满足条件(如数据到达)时,通过中断机制(如网卡或时钟中断)更新就绪列表,唤醒等待的进程。
25 6
|
3天前
|
NoSQL Redis 缓存
【后端面经】【缓存】36|Redis 单线程:为什么 Redis 用单线程而 Memcached 用多线程?
【5月更文挑战第17天】Redis常被称为单线程,但实际上其在处理命令时采用单线程,但在6.0后IO变为多线程。持久化和数据同步等任务由额外线程处理,因此严格来说Redis是多线程的。面试时需理解Redis的IO模型,如epoll和Reactor模式,以及其内存操作带来的高性能。Redis使用epoll进行高效文件描述符管理,实现高性能的网络IO。在讨论Redis与Memcached的线程模型差异时,应强调Redis的单线程模型如何通过内存操作和高效IO实现高性能。
30 7
【后端面经】【缓存】36|Redis 单线程:为什么 Redis 用单线程而 Memcached 用多线程?
|
6天前
|
缓存 NoSQL 关系型数据库
【Redis】Redis 缓存重点解析
【Redis】Redis 缓存重点解析
16 0
|
6天前
|
缓存 NoSQL 关系型数据库
【Redis】Redis作为缓存
【Redis】Redis作为缓存
8 0
|
6天前
|
存储 缓存 监控
利用Redis构建高性能的缓存系统
在现今高负载、高并发的互联网应用中,缓存系统的重要性不言而喻。Redis,作为一款开源的、内存中的数据结构存储系统,它可以用作数据库、缓存和消息代理。本文将深入探讨Redis的核心特性,以及如何利用Redis构建高性能的缓存系统,并通过实际案例展示Redis在提升系统性能方面的巨大潜力。
|
6天前
|
存储 缓存 NoSQL
【技术分享】求取列表需求的redis缓存方案
【技术分享】求取列表需求的redis缓存方案
17 0
|
6天前
|
负载均衡 监控 NoSQL
Redis的几种主要集群方案
【5月更文挑战第15天】Redis集群方案包括主从复制(基础,读写分离,手动故障恢复)、哨兵模式(自动高可用,自动故障转移)和Redis Cluster(官方分布式解决方案,自动分片、容错和扩展)。此外,还有Codis、Redisson和Twemproxy等工具用于代理分片和负载均衡。选择方案需考虑应用场景、数据量和并发需求,权衡可用性、性能和扩展性。
49 2
|
6天前
|
存储 监控 负载均衡
保证Redis的高可用性是一个涉及多个层面的任务,主要包括数据持久化、复制与故障转移、集群化部署等方面
【5月更文挑战第15天】保证Redis高可用性涉及数据持久化、复制与故障转移、集群化及优化策略。RDB和AOF是数据持久化方法,哨兵模式确保故障自动恢复。Redis Cluster实现分布式部署,提高负载均衡和容错性。其他措施包括身份认证、多线程、数据压缩和监控报警,以增强安全性和稳定性。通过综合配置与监控,可确保Redis服务的高效、可靠运行。
34 2
|
6天前
|
存储 NoSQL Redis
Redis源码、面试指南(5)多机数据库、复制、哨兵、集群(下)
Redis源码、面试指南(5)多机数据库、复制、哨兵、集群
26 1