当前位置:   article > 正文

004-Redis-缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级_在访问key之前,采用setnx(set if not exists)来设置另一个短期key来锁住当

在访问key之前,采用setnx(set if not exists)来设置另一个短期key来锁住当前key的

一、缓存雪崩

我们可以简单的理解为:由于原有缓存失效,新缓存未到期间 (例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

解决办法:

  • 大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开。

解决方案:

  • 错开缓存的过期时间,可通过设置缓存数据的过期时间为默认值基础上加上一个随机值,防止同一时间大量数据过期现象发生。
  • 搭建高可用的redis集群,避免出现缓存服务器宕机引起的雪崩问题。
  • 参照hystrix,进行熔断降级。
二、缓存穿透

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。

缓存雪崩是指缓存中数据大批量到过期时间,引发的大部分缓存突然同时不可用,而查询数据量巨大,引起数据库压力过大甚至宕机的情况。

需要注意缓存击穿和缓存雪崩的不同之处缓存击穿指的是大量的并发请求去查询同一条数据;而缓存雪崩是大量缓存同时过期,导致很多查询请求都查不到缓存数据从而查数据库。

解决办法:

  • 最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
  • 另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。

方法一 布隆过滤器

5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢?

对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。
Bitmap: 典型的就是哈希表
缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。

布隆过滤器(推荐)

就是引入了k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。

  • Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。

Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。

受提醒补充:缓存穿透与缓存击穿的区别

**缓存击穿:**是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据。
解决方案:在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。

来看一下应对方案:

1、缓存空对象

修改数据库写回缓存逻辑,对于缓存中不存在,数据库中也不存在的数据,我们仍然将其缓存起来,并且设置一个缓存过期时间。

请添加图片描述

如上图所示,查询数据库失败时,仍以查询的key值缓存一个空对象(key,null)。但是这么做仍然存在不少问题:

a、这时在缓存中查找这个key值时,会返回一个null的空对象。需要注意的是这个空对象可能并不是客户端需要的,所以需要对结果为空进行处理后,再返回给客户端
b、占用redis中大量内存。因为空对象能够被缓存,redis会使用大量的内存来存储这些值为空的key
c、如果在写缓存后数据库中存入的这个key的数据,由于缓存没有过期,取到的仍为空值,所以可能出现短暂的数据不一致问题

2、布隆过滤器

布隆过滤器是一个二进制向量,或者说二进制的数组,或者说是位(bit)数组。

请添加图片描述

因为是二进制的向量,它的每一位只能存放0或者1。当需要向布隆过滤器中添加一个数据映射时,添加的并不是原始的数据,而是使用多个不同的哈希函数生成多个哈希值,并将每个生成哈希值指向的下标位置置为1。所以,别再说从布隆过滤器中取数据啦,我们根本就没有存原始数据。

请添加图片描述
例如"Hydra"的三个哈希函数生成的下标分别为1,3,6,那么将这三位置为1,其他数据以此类推。**那么这样的数据结构能够起到什么效果呢?**我们可以根据这个位向量,来判断数据是否存在。

具体流程:

  • a、计算数据的多个哈希值;

  • b、判断这些bit是否为1,全部为1,则数据可能存在;

  • c、若其中一个或多个bit不为1,则判断数据不存在。

需要注意,布隆过滤器是存在误判的,因为随着数据存储量的增加,被置为1的bit数量也会增加,因此,有可能在查询一个并不存在的数据时,碰巧所有bit都已经被其他数据置为了1,也就是发生了哈希碰撞。因此,布隆过滤器只能做到判断数据是否可能存在,不能做到百分百的确定。

Google的guava包为我们提供了单机版的布隆过滤器实现,来看一下具体使用

首先引入maven依赖:

复制代码

<dependency>

    <groupId>com.google.guava</groupId>
    
    <artifactId>guava</artifactId>
    
    <version>27.1-jre</version>

</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

向布隆过滤器中模拟传入1000000条数据,给定误判率,再使用不存在的数据进行判断:

复制代码

public class BloomTest {
    public static void test(int dataSize,double errorRate){ 
        BloomFilter<Integer> bloomFilter=    
                BloomFilter.create(Funnels.integerFunnel(), dataSize, errorRate);
        for(int i = 0; i< dataSize; i++){
            bloomFilter.put(i);
        }
        int errorCount=0;
        for(int i = dataSize; i<2* dataSize; i++){
            if(bloomFilter.mightContain(i)){
                errorCount++;
            }
        }
        System.out.println("Total error count: "+errorCount);
    }    
    public static void main(String[] args) {    
        BloomTest.test(1000000,0.01);    
        BloomTest.test(1000000,0.001);    
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

测试结果:

Total error count: 10314

Total error count: 994
  • 1
  • 2
  • 3

可以看出,在给定误判率为0.01时误判了10314次,在误判率为0.001时误判了994次,大体符合我们的期望。

但是因为guava的布隆过滤器是运行在的jvm内存中,所以仅支持单体应用,并不支持微服务分布式。那么有没有支持分布式的布隆过滤器呢,这时Redis站了出来,自己造成的问题自己来解决!

Redis的**BitMap(位图)**支持了对位的操作,通过一个bit位来表示某个元素对应的值或者状态。

//对key所存储的字符串值,设置或清除指定偏移量上的位(bit)

setbit key offset value

//对key所存储的字符串值,获取指定偏移量上的位(bit)

getbit key offset
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

既然布隆过滤器是对位进行赋值,我们就可以使用BitMap提供的setbit和getbit命令非常简单的对其进行实现,并且setbit操作可以实现自动数组扩容,所以不用担心在使用过程中数组位数不够的情况。

复制代码

public class RedisBloomTest {
private static int dataSize = 1000;
private static double errorRate = 0.01;

//bit数组长度
private static long numBits;
//hash函数数量
private static int numHashFunctions;
public static void main(String[] args) {
    numBits = optimalNumOfBits(dataSize, errorRate);
    numHashFunctions = optimalNumOfHashFunctions(dataSize, numBits);
    System.out.println("Bits length: "+numBits);
    System.out.println("Hash nums: "+numHashFunctions);
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    for (int i = 0; i <= 1000; i++) {
        long[] indexs = getIndexs(String.valueOf(i));
        for (long index : indexs) {
            jedis.setbit("bloom", index, true);
        }
    }
    num:
    for (int i = 1000; i < 1100; i++) {
        long[] indexs = getIndexs(String.valueOf(i));
        for (long index : indexs) {
            Boolean isContain = jedis.getbit("bloom", index);
            if (!isContain) {
                System.out.println(i + "不存在");
                continue  num;
            }
        }
        System.out.println(i + "可能存在");
    }
}
//根据key获取bitmap下标
private static long[] getIndexs(String key) {
    long hash1 = hash(key);
    long hash2 = hash1 >>> 16;
    long[] result = new long[numHashFunctions];
    for (int i = 0; i < numHashFunctions; i++) {
        long combinedHash = hash1 + i * hash2;
        if (combinedHash < 0) {
            combinedHash = ~combinedHash;
        }
        result[i] = combinedHash % numBits;
    }
    return result;
}
private static long hash(String key) {
    Charset charset = Charset.forName("UTF-8");
    return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();
}
//计算hash函数个数
private static int optimalNumOfHashFunctions(long n, long m) {
    return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
//计算bit数组长度
  private static long optimalNumOfBits(long n, double p) {
      if (p == 0) {
          p = Double.MIN_VALUE;
      }
      return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
  }
}
  • 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
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

基于BitMap实现分布式布隆过滤器的过程中,哈希函数的数量以及位数组的长度都是动态计算的。可以说,给定的容错率越低,哈希函数的个数则越多,数组长度越长,使用的redis内存开销越大。

guava中布隆过滤器的数组最大长度是由int值的上限决定的,大概为21亿,而redis的位数组为512MB,也就是2^32位,所以最大长度能够达到42亿,容量为guava的两倍。

方法二 加分布式锁

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/159947
推荐阅读
相关标签