redis:缓存穿透,缓存雪崩,缓存和数据库的一致性,布隆过滤

缓存穿透

什么是缓存穿透?

缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,
导致请求直接到了数据库上,根本没有经过缓存这一层。
举个例子:
某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。

最终导致:
	用户的请求最终都要跑到数据库中查询一遍。

有哪些解决办法?

最基本的就是首先做好参数校验,
一些不合法的参数请求直接抛出异常信息返回给客户端。
比如查询的数据库 id 不能小于 0、
传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

1)缓存无效 key

如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,
具体命令如下: SET key value EX 10086 。
这种方式可以解决请求的 key 变化不频繁的情况,
如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。
很明显,这种方案并不能从根本上解决此问题。
如果非要用这种方式来解决穿透问题的话,
尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
一般情况下我们是这样设计 key 的: 表名:列名:主键名:主键值 。
如果用 Java 代码展示的话,差不多是下面这样的:
public Object getObjectInclNullById(Integer id) {
    // 从缓存中获取数据
    Object cacheValue = cache.get(id);
    // 缓存为空
    if (cacheValue == null) {
        // 从数据库中获取
        Object storageValue = storage.get(key);
        // 缓存空对象
        cache.set(key, storageValue);
        // 如果存储数据为空,需要设置一个过期时间(300秒)
        if (storageValue == null) {
            // 必须设置过期时间,否则有被攻击的风险
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    }
    return cacheValue;
}

2)布隆过滤器

布隆过滤器是一个非常神奇的数据结构,
通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。

具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,
当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。
不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

缓存雪崩

缓存雪崩描述的就是这样一个简单的场景:
缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,
造成数据库短时间内承受大量请求。 这就好比雪崩一样,摧枯拉朽之势,
数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
举个例子:系统的缓存模块出了问题比如宕机导致不可用。
造成系统的所有访问,都要走数据库。

还有一种缓存雪崩的场景是:
有一些被大量访问数据(热点缓存)在某一时刻大面积失效,
导致对应的请求直接落到了数据库上。 这样的情况,有下面几种解决办法:

举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,
设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,
这些秒杀的商品的访问直接就失效了。导致的情况就是,
相应的请求直接就落到了数据库上,就像雪崩一样可怕。

解决办法

针对 Redis 服务不可用的情况:
		采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
		限流,避免同时处理大量的请求。
	
针对热点缓存失效的情况:
		设置不同的失效时间比如随机设置缓存的失效时间。
		缓存永不失效。

保证缓存和数据库数据的一致性

** Cache Aside Pattern(旁路缓存模式)**

Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。
如果更新数据库成功,而删除缓存这一步失败的情况的话,
简单说两个解决方案:
缓存失效时间变短(不推荐,治标不治本) :
我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。
另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。

增加 cache 更新重试机制(常用): 
如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,
重试次数可以自己定。如果多次重试还是失败的话,
我们可以把当前更新失败的 key 存入队列中,
等缓存服务可用之后,再将 缓存中对应的 key 删除即可。

布隆过滤器

什么是布隆过滤器?

布隆过滤器(Bloom Filter)是一个叫做 Bloom 的老哥于1970年提出的。
我们可以把它看作由二进制向量(或者说位数组)
和一系列随机映射函数(哈希函数)两部分组成的数据结构。
相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,
但是缺点是其返回的结果是概率性的,而不是非常准确的。
理论情况下添加到集合中的元素越多,误报的可能性就越大。
并且,存放在布隆过滤器的数据不容易删除。

redis:缓存穿透,缓存雪崩,缓存和数据库的一致性,布隆过滤_第1张图片

位数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。
这样申请一个 100w 个元素的位数组
只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 kb ≈ 122kb 的空间。

总结:一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构,
这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。
并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大。

布隆过滤器的原理

当一个元素加入布隆过滤器中的时候,会进行如下操作:
	使用布隆过滤器中的哈希函数对元素值进行计算,
		得到哈希值(有几个哈希函数得到几个哈希值)。
	根据得到的哈希值,在位数组中把对应下标的值置为 1。

当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:
	对给定元素再次进行相同的哈希计算;
	得到值之后判断位数组中的每个元素是否都为 1,
			如果值都为 1,那么说明这个值在布隆过滤器中,
			如果存在一个值不为 1,说明该元素不在布隆过滤器中。

redis:缓存穿透,缓存雪崩,缓存和数据库的一致性,布隆过滤_第2张图片

如图所示,当字符串存储要加入到布隆过滤器中时,
该字符串首先由多个哈希函数生成不同的哈希值,
然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为0)。
当第二次存储相同字符串时,因为先前的对应位置已设置为 1,
所以很容易知道此值已经存在(去重非常方便)。

如果我们需要判断某个字符串是否在布隆过滤器中时,
只需要对给定字符串再次进行相同的哈希计算,
得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,
那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

不同的字符串可能哈希出来的位置相同,
这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。

综上,我们可以得出:布隆过滤器说某个元素存在,小概率会误判。
布隆过滤器说某个元素不在,那么这个元素一定不在。

布隆过滤器使用场景

判断给定数据是否存在:
比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,5亿以上!)、 
防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、
邮箱的垃圾邮件过滤、黑名单功能等等。

去重:比如爬给定网址的时候对已经爬取过的 URL 去重。

通过 Java 编程手动实现布隆过滤器

import java.util.BitSet;

public class MyBloomFilter {

    /**
     * 位数组的大小
     */
    private static final int DEFAULT_SIZE = 2 << 24;
    /**
     * 通过这个数组可以创建 6 个不同的哈希函数
     */
    private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};

    /**
     * 位数组。数组中的元素只能是 0 或者 1
     */
    private BitSet bits = new BitSet(DEFAULT_SIZE);

    /**
     * 存放包含 hash 函数的类的数组
     */
    private SimpleHash[] func = new SimpleHash[SEEDS.length];

    /**
     * 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样
     */
    public MyBloomFilter() {
        // 初始化多个不同的 Hash 函数
        for (int i = 0; i < SEEDS.length; i++) {
            func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
        }
    }

    /**
     * 添加元素到位数组
     */
    public void add(Object value) {
        for (SimpleHash f : func) {
            bits.set(f.hash(value), true);
        }
    }

    /**
     * 判断指定元素是否存在于位数组
     */
    public boolean contains(Object value) {
        boolean ret = true;
        for (SimpleHash f : func) {
            ret = ret && bits.get(f.hash(value));
        }
        return ret;
    }

    /**
     * 静态内部类。用于 hash 操作!
     */
    public static class SimpleHash {

        private int cap;
        private int seed;

        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        /**
         * 计算 hash 值
         */
        public int hash(Object value) {
            int h;
            return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16)));
        }
    }
}
测试:
        String value1 = "https://java.com";
        String value2 = "https://github.com";
        MyBloomFilter filter = new MyBloomFilter();
        System.out.println(filter.contains(value1));
        System.out.println(filter.contains(value2));
        filter.add(value1);
        filter.add(value2);
        System.out.println(filter.contains(value1));
        System.out.println(filter.contains(value2));
Output:
		false
		false
		true
		true

Google开源的 Guava中自带的布隆过滤器

项目中引入 Guava 的依赖


    com.google.guava
    guava
    28.0-jre

我们创建了一个最多存放 最多 1500个整数的布隆过滤器,
并且我们可以容忍误判的概率为百分之(0.01)
  // 创建布隆过滤器对象
        BloomFilter filter = BloomFilter.create(
                Funnels.integerFunnel(),
                1500,
                0.01);
        // 判断指定元素是否存在
        System.out.println(filter.mightContain(1));
        System.out.println(filter.mightContain(2));
        // 将元素添加进布隆过滤器
        filter.put(1);
        filter.put(2);
        System.out.println(filter.mightContain(1));
        System.out.println(filter.mightContain(2));
在我们的示例中,当mightContain() 方法返回true时,
我们可以99%确定该元素在过滤器中,当过滤器返回false时,
我们可以100%确定该元素不存在于过滤器中。

Guava 提供的布隆过滤器的实现还是很不错的(想要详细了解的可以看一下它的源码实现),
但是它有一个重大的缺陷就是只能单机使用(另外,容量扩展也不容易),
而现在互联网一般都是分布式的场景。
为了解决这个问题,我们就需要用到 Redis 中的布隆过滤器了。

Redis 中的布隆过滤器

使用Docker安装
如果我们需要体验 Redis 中的布隆过滤器非常简单,
通过 Docker 就可以了!我们直接在 Google 搜索docker redis bloomfilter 
具体地址:https://hub.docker.com/r/redislabs/rebloom/ 
~ docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest
~ docker exec -it redis-redisbloom bash
root@21396d02c252:/data# redis-cli
127.0.0.1:6379> 

常用命令

注意: key:布隆过滤器的名称,item : 添加的元素。

BF.ADD :将元素添加到布隆过滤器中,
如果该过滤器尚不存在,则创建该过滤器。
格式:BF.ADD {key} {item}。

BF.MADD : 将一个或多个元素添加到“布隆过滤器”中,
并创建一个尚不存在的过滤器。
该命令的操作方式BF.ADD与之相同,
只不过它允许多个输入并返回多个值。
格式:BF.MADD {key} {item} [item ...] 。

BF.EXISTS : 确定元素是否在布隆过滤器中存在。
格式:BF.EXISTS {key} {item}。

BF.MEXISTS : 确定一个或者多个元素是否在布隆过滤器中存在
格式:BF.MEXISTS {key} {item} [item ...]

另外,BF.RESERVE 命令需要单独介绍一下:
这个命令的格式如下:
BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion] 。

下面简单介绍一下每个参数的具体含义:

		key:布隆过滤器的名称
		error_rate :误报的期望概率。这应该是介于0到1之间的十进制值。
			例如,对于期望的误报率0.1%(1000中为1),error_rate应该设置为0.001。
			该数字越接近零,则每个项目的内存消耗越大,并且每个操作的CPU使用率越高。
			
		capacity: 过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。
			实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。
			
		可选参数:
				expansion:如果创建了一个新的子过滤器,
				则其大小将是当前过滤器的大小乘以expansion。默认扩展值为2。
				这意味着每个后续子过滤器将是前一个子过滤器的两倍。
例如:
127.0.0.1:6379> BF.ADD myFilter java
(integer) 1
127.0.0.1:6379> BF.ADD myFilter python
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter java
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter python
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter github
(integer) 0

你可能感兴趣的:(redis)