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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容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
相关文章
|
3天前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
117 85
|
1月前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
24天前
|
缓存 NoSQL 数据库
缓存穿透、缓存击穿和缓存雪崩及其解决方案
在现代应用中,缓存是提升性能的关键技术之一。然而,缓存系统也可能遇到一系列问题,如缓存穿透、缓存击穿和缓存雪崩。这些问题可能导致数据库压力过大,甚至系统崩溃。本文将探讨这些问题及其解决方案。
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
40 5
|
2月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
102 1
|
2月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
53 2
数据的存储--Redis缓存存储(二)
|
2月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
79 6
|
2月前
|
缓存 NoSQL 关系型数据库
redis和缓存及相关问题和解决办法 什么是缓存预热、缓存穿透、缓存雪崩、缓存击穿
本文深入探讨了Redis缓存的相关知识,包括缓存的概念、使用场景、可能出现的问题(缓存预热、缓存穿透、缓存雪崩、缓存击穿)及其解决方案。
224 0
redis和缓存及相关问题和解决办法 什么是缓存预热、缓存穿透、缓存雪崩、缓存击穿
|
1月前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构
|
1月前
|
缓存 NoSQL Redis
Redis 缓存使用的实践
《Redis缓存最佳实践指南》涵盖缓存更新策略、缓存击穿防护、大key处理和性能优化。包括Cache Aside Pattern、Write Through、分布式锁、大key拆分和批量操作等技术,帮助你在项目中高效使用Redis缓存。
256 22