Redis使用元素删除的布隆过滤器来解决缓存穿透问题

目录
  • 前言
  • 缓存雪崩
    • 解决方案
  • 缓存击穿
    • 解决方案
  • 缓存穿透
    • 解决方案
  • 布隆过滤器(Bloom Filter)
    • 什么是布隆过滤器
    • 位图(Bitmap)
    • 哈希碰撞
    • 布隆过滤器的2大特点
      • fpp
    • 布隆过滤器的实现(Guava)
    • 布隆过滤器的如何删除
      • 带有计数器的布隆过滤器
  • 总结

前言

在我们日常开发中,Redis使用场景最多的就是作为缓存和分布式锁等功能来使用,而其用作缓存最大的目的就是为了降低数据库访问。但是假如我们某些数据并不存在于Redis当中,那么请求还是会直接到达数据库,而一旦在同一时间大量缓存失效或者一个不存在缓存的请求被恶意访问,这些都会导致数据库压力骤增,这就是本文要讲述的缓存穿透,缓存击穿和缓存雪崩的问题,而布隆过滤器正是缓存穿透的一种解决方案

缓存雪崩

缓存雪崩指的是Redis当中的大量缓存在同一时间全部失效,而假如恰巧这一段时间同时又有大量请求被发起,那么就会造成请求直接访问到数据库,可能会把数据库冲垮。

缓存雪崩一般形容的是缓存中没有而数据库中有的数据,而因为时间到期导致请求直达数据库。

解决方案

解决缓存雪崩的方法有很多:

1、加锁,保证单线程访问缓存。这样就不会有很多请求同时访问到数据库。

2、失效时间不要设置成一样。典型的就是初始化预热数据的时候,将数据存入缓存时可以采用随机时间来确保不会咋同一时间有大量缓存失效。

3、内存允许的情况下,可以将缓存设置为永不失效。

缓存击穿

缓存击穿和缓存雪崩很类似,区别就是缓存击穿一般指的是单个缓存失效,而同一时间又有很大的并发请求需要访问这个key,从而造成了数据库的压力。

解决方案

解决缓存击穿的方法和解决缓存雪崩的方法很类似:

1、加锁,保证单线程访问缓存。这样第一个请求到达数据库后就会重新写入缓存,后续的请求就可以直接读取缓存。2、内存允许的情况下,可以将缓存设置为永不失效。

缓存穿透

缓存穿透和上面两种现象的本质区别就是这时候访问的数据其在数据库中也不存在,那么既然数据库不存在,所以缓存里面肯定也不会存在,这样如果并发过大就会造成数据源源不断的到达数据库,给数据库造成极大压力。

解决方案

对于缓存穿透问题,加锁并不能起到很好地效果,因为本身key就是不存在,所以即使控制了线程的访问数,但是请求还是会源源不断的到达数据库。

解决缓存穿透问题一般可以采用以下方案配合使用:

1、接口层进行校验,发现非法的key直接返回。比如数据库中采用的是自增id,那么如果来了一个非整型的id或者负数id可以直接返回,或者说如果采用的是32位uuid,那么发现id长度不等于32位也可以直接返回。

2、将不存在的数据也进行缓存,可以直接缓存一个空或者其他约定好的无效value。采用这种方案最好将key设置一个短期失效时间,否则大量不存在的key被存储到Redis中,也会占用大量内存。

布隆过滤器(Bloom Filter)

针对上面缓存穿透的解决方案,我们思考一下:假如一个key可以绕过第1种方法的校验,而此时有大量的不存在key被访问(如1亿个或者10亿个),那么这时候全部存储到缓存,会占用非常大的空间,会浪费大量服务器内存,导致内存不足。

那么有没有一种更好的解决方案呢?这就是我们接下来要介绍的布隆过滤器,布隆过滤器就可以最大程度的解决key值过多的这个问题。

什么是布隆过滤器

可能大部分人都知道有这么一个面试问题:如何在10亿的海量的无序的数据中快速判断一个元素是否存在?

要解决这个问题就需要用到布隆过滤器,否则大部分服务器的内存是无法存储这么大的数量级的数据的。

布隆过滤器(Bloom Filter)是由布隆在1970年提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率而且删除困难。

位图(Bitmap)

Redis当中有一种数据结构就是位图,布隆过滤器其中重要的实现就是位图的实现,也就是位数组,并且在这个数组中每一个位置只有0和1两种状态,每个位置只占用1个比特(bit),其中0表示没有元素存在,1表示有元素存在。如下图所示就是一个简单的布隆过滤器示例(一个key值经过哈希运算和位运算就可以得出应该落在哪个位置):

哈希碰撞

上面我们发现,lonelywolf落在了同一个位置,这种不同的key值经过哈希运算后得到相同值的现象就称之为哈希碰撞。发生哈希碰撞之后再经过位运算,那么最后肯定会落在同一个位置。

如果发生过多的哈希碰撞,就会影响到判断的准确性,所以为了减少哈希碰撞,我们一般会综合考虑以下2个因素:

1、增大位图数组的大小(位图数组越大,占用的内存越大)。

2、增加哈希函数的次数(同一个key值经过1个函数相等了,那么经过2个或者更多个哈希函数的计算,都得到相等结果的概率就自然会降低了)。

上面两个方法我们需要综合考虑:比如增大位数组,那么就需要消耗更多的空间,而经过越多的哈希计算也会消耗cpu影响到最终的计算时间,所以位数组到底多大,哈希函数次数又到底需要计算多少次合适需要具体情况具体分析。

布隆过滤器的2大特点

下面这个就是一个经过了2次哈希函数得到的布隆过滤器,根据下图我们很容易看到,假如我们的Redis根本不存在,但是Redis经过2次哈希函数之后得到的两个位置已经是1了(一个是wolf通过f2得到,一个是Nosql通过f1得到)。

所以通过上面的现象,我们从布隆过滤器的角度可以得出布隆过滤器主要有2大特点:

1、如果布隆过滤器判断一个元素存在,那么这个元素可能存在

2、如果布隆过滤器判断一个元素不存在,那么这个元素一定不存在

而从元素的角度也可以得出2大特点:

1、如果元素实际存在,那么布隆过滤器一定会判断存在

2、如果元素不存在,那么布隆过滤器可能会判断存在

PS:需要注意的是,如果经过N次哈希函数,则需要得到的N个位置都是1才能判定存在,只要有一个是0,就可以判定为元素不存在布隆过滤器中

fpp

因为布隆过滤器中总是会存在误判率,因为哈希碰撞是不可能百分百避免的。布隆过滤器对这种误判率称之为假阳性概率,即:False Positive Probability,简称为fpp。

在实践中使用布隆过滤器时可以自己定义一个fpp,然后就可以根据布隆过滤器的理论计算出需要多少个哈希函数和多大的位数组空间。需要注意的是这个fpp不能定义为100%,因为无法百分保证不发生哈希碰撞。

布隆过滤器的实现(Guava)

在Guava的包中提供了布隆过滤器的实现,下面就通过Guava来体会一下布隆过滤器的应用:
1、引入pom依赖

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

2、新建一个布隆过滤器的测试demo:

package com.lonelyWolf.redis;

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class BloomFilterDemo {
    private static final int expectedInsertions = 1000000;

    public static void main(String[] args) {
        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),expectedInsertions);

        List<String> list = new ArrayList<>(expectedInsertions);

        for (int i = 0; i < expectedInsertions; i++) {
            String uuid = UUID.randomUUID().toString();
            bloomFilter.put(uuid);
            list.add(uuid);
        }

        int rightNum1 = 0;
        int wrongNum1 = 0;

        NumberFormat percentFormat =NumberFormat.getPercentInstance();
        percentFormat.setMaximumFractionDigits(2); //最大小数位数

        for (int i=0;i < 500;i++){
            String key = list.get(i);
            if (bloomFilter.mightContain(key)){
                if (list.contains(key)){
                    rightNum1++;
                }else {
                    wrongNum1++;
                }
            }
        }
        System.out.println("布隆过滤器认为存在的key值数:" + rightNum1);
        System.out.println("-----------------------分割线---------------------------------");

        int rightNum2 = 0;
        int wrongNum2 = 0;

        for (int i=0;i < 5000;i++){
            String key = UUID.randomUUID().toString();
            if (bloomFilter.mightContain(key)){
                if (list.contains(key)){
                    rightNum2++;
                }else {
                    wrongNum2++;
                }
            }
        }

        System.out.println("布隆过滤器认为存在的key值数:" + rightNum2);
        System.out.println("布隆过滤器认为不存在的key值数:" + wrongNum2);
        System.out.println("布隆过滤器的误判率为:" + percentFormat.format((float)wrongNum2 / 5000));
    }
}

运行之后,第一部分输出的值一定是和for循环内的值相等,也就是百分百匹配,即满足了原则1:如果元素实际存在,那么布隆过滤器一定会判断存在
第二部分的输出的误判率即fpp总是在3%左右,而且随着for循环的次数越大,越接近3%。即满足了原则2:如果元素不存在,那么布隆过滤器可能会判断存在

这个3%的误判率是如何来的呢?我们进入创建布隆过滤器的create方法,发现默认的fpp就是0.03:

对于这个默认的3%的fpp需要多大的位数组空间和多少次哈希函数得到的呢?在BloomFilter类下面有两个default方法可以获取到位数组空间大小和哈希函数的个数:

  • optimalNumOfHashFunctions:获取哈希函数的次数
  • optimalNumOfBits:获取位数组大小

debug进去看一下:

得到的结果是7298440 bit=0.87M,然后经过了5次哈希运算。可以发现这个空间占用是非常小的,100W的key才占用了0.87M。

PS:点击这里可以进入网站计算bit数组大小和哈希函数个数。

布隆过滤器的如何删除

上面的布隆过滤器我们知道,判断一个元素存在就是判断对应位置是否为1来确定的,但是如果要删除掉一个元素是不能直接把1改成0的,因为这个位置可能存在其他元素,所以如果要支持删除,那我们应该怎么做呢?最简单的做法就是加一个计数器,就是说位数组的每个位如果不存在就是0,存在几个元素就存具体的数字,而不仅仅只是存1,那么这就有一个问题,本来存1就是一位就可以满足了,但是如果要存具体的数字比如说2,那就需要2位了,所以带有计数器的布隆过滤器会占用更大的空间

带有计数器的布隆过滤器

下面就是一个带有计数器的布隆过滤器示例
1、引入依赖:

<dependency>
    <groupId>com.baqend</groupId>
    <artifactId>bloom-filter</artifactId>
    <version>1.0.7</version>
</dependency>

2、新建一个带有计数器的布隆过滤器demo:

package com.lonelyWolf.redis.bloom;

import orestes.bloomfilter.FilterBuilder;

public class CountingBloomFilter {
    public static void main(String[] args) {
        orestes.bloomfilter.CountingBloomFilter<String> cbf = new FilterBuilder(10000,
                0.01).countingBits(8).buildCountingBloomFilter();

        cbf.add("zhangsan");
        cbf.add("lisi");
        cbf.add("wangwu");
        System.out.println("是否存在王五:" + cbf.contains("wangwu")); //true
        cbf.remove("wangwu");
        System.out.println("是否存在王五:" + cbf.contains("wangwu")); //false
    }
}

构建布隆过滤器前面2个参数一个就是期望的元素数,一个就是fpp值,后面的countingBits参数就是计数器占用的大小,这里传了一个8位,即最多允许255次重复,如果不传的话这里默认是16位大小,即允许65535次重复。

总结

本文主要讲述了使用Redis存在的三种问题:缓存雪崩缓存击穿缓存穿透。并分别对每种问题的解决方案进行了描述,最后着重介绍了缓存穿透的解决方案:布隆过滤器。原生的布隆过滤器不支持删除,但是可以引入一个计数器实现带有计数器的布隆过滤器来实现删除功能,同时在最后也提到了,带有计数器的布隆过滤器会占用更多的空间问题。

到此这篇关于Redis使用元素删除的布隆过滤器来解决缓存穿透问题的文章就介绍到这了,更多相关Redis 缓存穿透内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • redis缓存穿透解决方法

    缓存技术可以用来减轻数据库的压力,提升访问效率.目前在企业项目中对缓存也是越来越重视.但是缓存不是说随随便便加入项目就可以了.将缓存整合到项目中,这才是第一步.而缓存带来的穿透问题,进而导致的雪蹦问题都是我们迫切需要解决的问题.本篇文章将我平时项目中的解决方案分享给大家,以供参考. 一.缓存穿透的原理 缓存的正常使用如图: 如图所示,缓存的使用流程: 1.先从缓存中取数据,如果能取到,则直接返回数据给用户.这样不用访问数据库,减轻数据库的压力. 2.如果缓存中没有数据,就会访问数据库. 这里面就

  • Redis缓存穿透出现原因及解决方案

    在并发式的项目当中,一定要考虑一个缓存穿透的情况.那么什么是缓存穿透呢?简单的说来,就是当大量请求的key根本不在缓存当中,所以导致了请求直接到了数据库上,根本没有经过缓存这一层.比如一个黑客故意制造我们缓存中不存在的key发送大量的请求,就会导致请求直接落到数据库上. 也就是说,缓存穿透就是:1.缓存层不命中.2,存储层不命中,不将空的结果写回缓存.3,返回空结果给客户端. 一般mysql的默认最大连接数是150左右,当然这个是可以用show variables like '%max_conn

  • springboot中redis的缓存穿透问题实现

    什么是缓存穿透问题?? 我们使用redis是为了减少数据库的压力,让尽量多的请求去承压能力比较大的redis,而不是数据库.但是高并发条件下,可能会在redis还没有缓存的时候,大量的请求同时进入,导致一大批的请求直奔数据库,而不会经过redis.使用代码模拟缓存穿透问题如下: 首先是service里面的代码: @Service public class NewsService { @Autowired private NewsDAO newsDAO; //springboot自动初始化,不需要

  • Redis使用元素删除的布隆过滤器来解决缓存穿透问题

    目录 前言 缓存雪崩 解决方案 缓存击穿 解决方案 缓存穿透 解决方案 布隆过滤器(Bloom Filter) 什么是布隆过滤器 位图(Bitmap) 哈希碰撞 布隆过滤器的2大特点 fpp 布隆过滤器的实现(Guava) 布隆过滤器的如何删除 带有计数器的布隆过滤器 总结 前言 在我们日常开发中,Redis使用场景最多的就是作为缓存和分布式锁等功能来使用,而其用作缓存最大的目的就是为了降低数据库访问.但是假如我们某些数据并不存在于Redis当中,那么请求还是会直接到达数据库,而一旦在同一时间大

  • 布隆过滤器面试如何快速判断元素是否在集合里

    目录 1.什么叫布隆过滤器 2.实现原理 3.作用 4.具体实现 5.代码的实现 6.实战 7.小结 如何快速判断一个元素是不是在一个集合里?这个题目是我最近面试的时候常问的一个问题,这个问题不同人都有很多不同的回答. 今天想介绍一个很少有人会提及到的方案,那就是借助布隆过滤器. 1.什么叫布隆过滤器 布隆过滤器(Bloom Filter)是一个叫做 Bloom 的老哥于1970年提出的. 实际上可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构. 它的

  • Java的布隆过滤器你了解吗

    目录 BitMap 布隆过滤器 运用场景 传统数据结构的不足 实现原理 误判现象 实现 Redis 的 bitmap RedisBloom Guava 的 BloomFilter Redisson 解决缓存穿透 总结 BitMap 现代计算机用二进制(bit,位)作为信息的基础单位,1 个字节等于 8 位,例如big字符串是由 3 个字节组成,但实际在计算机存储时将其用二进制表示,big分别对应的 ASCII 码分别是 98.105.103,对应的二进制分别是 01100010.01101001

  • 详解Redis缓存穿透/击穿/雪崩原理及其解决方案

    目录 1.简介 2.缓存穿透 2.1描述 2.2解决方案 3.缓存击穿 3.1描述 3.2解决方案 4.缓存雪崩 4.1描述 4.1解决方案 5.布隆过滤器 5.1描述 5.2数据结构 5.3"一定不在集合中" 5.4"可能在集合中" 5.5"删除困难" 5.6为什么不使用HashMap呢? 1. 简介 如图所示,一个正常的请求 1.客户端请求张铁牛的博客. 2.服务首先会请求redis,查看请求的内容是否存在. 3.redis将请求结果返回给服

  • Redis缓存穿透/击穿工具类的封装

    目录 1. 简单的步骤说明 2. 逻辑缓存数据类型 3. 缓冲工具类的封装 3.1 CacheClient 类的类图结构 3.2 CacheClient 类代码 1. 简单的步骤说明 创建一个逻辑缓存数据类型 封装缓冲穿透和缓冲击穿工具类 2. 逻辑缓存数据类型 这里主要是创建一个可以往Redis里边存放的数据类型 RedisData 的Java类型 import lombok.Data; import java.time.LocalDateTime; @Data public class Re

  • Python+Redis实现布隆过滤器

    布隆过滤器是什么 布隆过滤器(Bloom Filter)是1970年由布隆提出的.它实际上是一个很长的二进制向量和一系列随机映射函数.布隆过滤器可以用于检索一个元素是否在一个集合中.它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难. 布隆过滤器的基本思想 通过一种叫作散列表(又叫哈希表,Hash table)的数据结构.它可以通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点.这样一来,我们只要看看这个点是不是1就可以知道集合中有没

  • SpringBoot+Redis布隆过滤器防恶意流量击穿缓存

    目录 什么是恶意流量穿透 怎么防 布隆过滤器的另一个用武场景 给Redis安装BloomFilter 在Redis里使用BloomFilter 结合SpringBoot使用 搭建springboot工程 使用压测工具喂120万条数据进入RedisBloomfilter看实际效果 本文主要介绍了SpringBoot+Redis布隆过滤器防恶意流量击穿缓存,具体如下: 什么是恶意流量穿透 假设我们的Redis里存有一组用户的注册email,以email作为Key存在,同时它对应着DB里的User表的

  • Redis 中的布隆过滤器的实现

    什么是『布隆过滤器』 布隆过滤器是一个神奇的数据结构,可以用来判断一个元素是否在一个集合中.很常用的一个功能是用来去重.在爬虫中常见的一个需求:目标网站 URL 千千万,怎么判断某个 URL 爬虫是否宠幸过?简单点可以爬虫每采集过一个 URL,就把这个 URL 存入数据库中,每次一个新的 URL 过来就到数据库查询下是否访问过. select id from table where url = 'https://jaychen.cc' 但是随着爬虫爬过的 URL 越来越多,每次请求前都要访问数据

  • SpringBoot+Redis实现布隆过滤器的示例代码

    目录 简述 Redis安装BloomFilter 基本指令 结合SpingBoot 方式一 方式二 简述 关于布隆过滤器的详细介绍,我在这里就不再赘述一遍了 我们首先知道:BloomFilter使用长度为m bit的字节数组,使用k个hash函数,增加一个元素: 通过k次hash将元素映射到字节数组中k个位置中,并设置对应位置的字节为1.查询元素是否存在: 将元素k次hash得到k个位置,如果对应k个位置的bit是1则认为存在,反之则认为不存在. Guava 中已经有具体的实现,而在我们实际生产

  • Redis 布隆过滤器命令的使用详解

    目录 一.Docker 安装 Redis 布隆过滤器 学习历史重要原因之一,就是要学会感恩,因为我们都是站在巨人的肩膀上. 1.1.安装 注意: 1.2.测试 二.RedisBloom 命令讲解 2.1.命令大纲 2.2.BF.ADD 和 BF.MADD 2.3.BF.EXISTS 和 BF.MEXISTS 2.4.BF.INFO 2.5.BF.RESERVE 2.6.BF.INSERT 因为平常使用 Docker 比较多,所以照常还是使用Docker来准备环境啦. 一.Docker 安装 Re

随机推荐