• 【雪崩、穿透、击穿、预热】概念、解决方案_Redis01


    1、缓存雪崩

    概念:

    • 缓存雪崩是指某一时刻发生了大规模的缓存失效,比如发生了 Redis 服务器宕机、缓存数据同时到期被删除这种情况,此时大量的请求直接转发到数据库,数据库一旦撑不住就会导致整个服务瘫痪。

    解决方案:

    • 1.分析用户行为,制定策略为 key 设置不同的过期时间,尽量让缓存失效的时间均匀分布

    • 2.采用主从架构 + Sentinel 或者 Redis Cluster 实现HA,避免 Redis 单点故障。

    • 3.设置本地缓存(ehcache) + 限流(hystrix)

      • 如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不再继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。服务降级的最终目的保证核心服务可用
    • 4.开启 Redis 持久化机制,服务重启后快速恢复缓存数据。

    • 5.互斥锁,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

    • 6.数据预热,数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。

    1)互斥锁:

     /*
     * 代码实现方案1
     */  
    
    //锁对象
    static Lock reenLock = new ReentrantLock()public List<String> getData04() throws InterruptedException {
        List<String> result = new ArrayList<String>()// 从缓存读取数据
        result = getDataFromCache()if (result.isEmpty()) {
            if (reenLock.tryLock()) {
                try {
                    System.out.println("我拿到锁了,从DB获取数据库后写入缓存")// 从数据库查询数据
                    result = getDataFromDB()// 将查询到的数据写入缓存
                    setDataToCache(result)} finally {
                    reenLock.unlock()// 释放锁
                }
    
            } else {
                result = getDataFromCache()// 先查一下缓存
                if (result.isEmpty()) {
                    System.out.println("我没拿到锁,缓存也没数据,先小憩一下")Thread.sleep(100)// 小憩一会儿
                    return getData04()// 重试
                }
            }
        }
        return result;
    }
    
    /*
     * 代码实现方案2
     */
    public String get(key) {
          String value = redis.get(key)if (value == null) { //代表缓存值过期
              //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
    		  if (redis.setnx(key_mutex, 13 * 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;      
          }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    2)缓存标记解决方案:

    • 分析:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!
    //伪代码
    public object GetProductListNew() {
        int cacheTime = 30String 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;
        }
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 解释说明:
      • 缓存标记:记录缓存标记是否过期,如果过期触发通知另外的线程在后台去更新实际key的缓存
      • 缓存数据:缓存数据的过期时间缓存标记的时间延长1倍,例:标记缓存时间30分钟数据缓存设置为60分钟。这样,当缓存标记key过期后实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后才会返回新缓存

    2、缓存穿透(两层都穿透)

    概念:

    • 缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

    解决方案:

    • 1.由于请求的参数是不合法的(每次都请求不存在的数据),于是我们可以使用布隆过滤器(BloomFilter)或者压缩 filter 提前拦截,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个bitmap 过滤掉,从而缓解底层存储系统的查询压力
    • 2.接口层增加校验,如用户鉴权校验,id 做基础校验,id<=0 的直接拦截
    • 3.缓存空对象:即便存储层查不到这个数据,也将返回的空对象设置到缓存里。下次再请求的时候,直接从缓存取到空对象返回,这种情况一般会将空对象设置一个较短的过期时间,这样可以防止攻击者反复用同一个 id 暴力攻击。

    缓存空对象代码演示:

    //伪代码
    public object GetProductListNew() {
        int cacheTime = 30String 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    3、缓存击穿(一层被穿透)

    概念:

    • 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
    • 和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

    解决方案

    • 方案1:加互斥锁
    • 方案2:设置热点数据永远不过期

    解决方案代码实现

    (1)互斥锁方案1:
     /*
     * 代码实现方案1
     */  
    static Lock reenLock = new ReentrantLock()public List<String> getData04() throws InterruptedException {
        List<String> result = new ArrayList<String>()// 从缓存读取数据
        result = getDataFromCache()if (result.isEmpty()) {
            if (reenLock.tryLock()) {
                try {
                    System.out.println("我拿到锁了,从DB获取数据库后写入缓存")// 从数据库查询数据
                    result = getDataFromDB()// 将查询到的数据写入缓存
                    setDataToCache(result)} finally {
                    reenLock.unlock()// 释放锁
                }
    
            } else {
                result = getDataFromCache()// 先查一下缓存
                if (result.isEmpty()) {
                    System.out.println("我没拿到锁,缓存也没数据,先小憩一下")Thread.sleep(100)// 小憩一会儿
                    return getData04()// 重试
                }
            }
        }
        return result;
    }
    /*
     * 代码实现方案2
     */
    public String get(key) {
          String value = redis.get(key)if (value == null) { //代表缓存值过期
              //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
    		  if (redis.setnx(key_mutex, 13 * 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;      
          }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    (2)热点数据永远不过期方案2:
    • 这里的“永远不过期”包含两层意思:
      • (1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
      • (2) 从功能上看,我们把过期时间存在key对应的value里,如果发现要过期了(缓存过期时间-当前系统时间<=1分钟(自定义的一个值)),通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。
    • 缺点:这种方案在特殊情况下也会有问题。假设缓存过期时间是12:00,而 11:59 到 12:00这 1 分钟时间里恰好没有 get 请求过来,又恰好请求都在 12:10 分的时候高并发过来,那就悲剧了。
    String get(final String key) {  
            V v = redis.get(key)//存的一个对象  
            String value = v.getValue()long timeout = v.getTimeout()if (System.currentTimeMillis() - timeout <= 一分钟) {//快要过期了
                // 异步更新后台异常执行  
                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;  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    4、redis预热

    概念

    • 提前给redis中嵌入部分数据,再提供服务。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据

    缓存预热的思路

    • a. 提前给redis中嵌入部分数据,再提供服务
    • b. 肯定不可能将所有数据都写入redis,因为数据量太大了,第一耗费的时间太长了,第二redis根本就容纳不下所有的数据
    • c. 需要根具当天的具体访问情况,试试统计出访问频率较高的热数据
    • d. 然后将访问频率较高的热数据写入到redis,肯定是热数据也比较多,我们也得多个服务并行的读取数据去写,并行的分布式的缓存预热
    • e. 然后将嵌入的热数据的redis对外提供服务,这样就不至于冷启动,直接让数据库奔溃了
  • 相关阅读:
    SIMULIA-Simpack 2022x新功能介绍
    aop-动态代理,cglib动态代理,面向切面编程,aop的实现方法
    前端研习录(11)——CSS3新特性——圆角及阴影讲解及示例说明
    Redis的Set类型、Sorted Set类型、Bitmap类型和HyperLogLog
    【OpenCV】-物体的凸包
    MATLAB环境下基于频率滑动广义互相关的信号时延估计方法
    PHP+MySQL制作简单动态网站(附详细注释+源码)
    Java多线程基础(创建、使用,状态)——Java第九讲
    【速通指南】《信息资源管理》信息系统资源管理,第3章
    优思学院|什么才是真正的精益化管理?-CLMP
  • 原文地址:https://blog.csdn.net/weixin_38963649/article/details/126154756