布隆过滤器及其使用实例

1、什么是布隆过滤器

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。

相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

2、实现原理

HashMap 的问题

讲述布隆过滤器的原理之前,我们先思考一下,通常你判断某个元素是否存在用的是什么?应该蛮多人回答 HashMap 吧,确实可以将值映射到 HashMap 的 Key,然后可以在 O(1) 的时间复杂度内返回结果,效率奇高。但是 HashMap 的实现也有缺点,例如存储容量占比高,考虑到负载因子的存在,通常空间是不能被用满的,而一旦你的值很多例如上亿的时候,那 HashMap 占据的内存大小就变得很可观了。

还比如说你的数据集存储在远程服务器上,本地服务接受输入,而数据集非常大不可能一次性读进内存构建 HashMap 的时候,也会存在问题。

布隆过滤器数据结构

布隆过滤器是一个 bit 向量或者说 bit 数组,如图:

图1

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “zhansan” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:

图2

我们现在再存一个值 “lisi”,如果哈希函数返回 3、4、8 的话,则图为:

图3

值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “wangwu” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “wangwu” 这个值不存在。而当我们需要查询 “zhansan” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “zhansan” 存在了么?答案是不可以,只能是 “zhansan” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在。

支持删除么

目前我们知道布隆过滤器可以支持 add 和 isExist 操作,那么 delete 操作可以么,答案是不可以,例如上图中的 bit 位 4 被两个值共同覆盖的话,一旦你删除其中一个值例如 “tencent” 而将其置位 0,那么下次判断另一个值例如 “baidu” 是否存在的话,会直接返回 false,而实际上你并没有删除它。

如何解决这个问题,答案是计数删除。但是计数删除需要存储一个数值,而不是原先的 bit 位,会增大占用的内存大小。这样的话,增加一个值就是将对应索引槽上存储的值加一,删除则是减一,判断是否存在则是看值是否大于0。

3、实例

使用布隆过滤器解决Redis缓存穿透问题

pom.xml

<parent>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-parent</artifactId>

    <version>2.0.1.RELEASE</version>

</parent>

<dependencies>

    <!-- 集成lombok 框架 -->

    <dependency>

        <groupId>org.projectlombok</groupId>

        <artifactId>lombok</artifactId>

    </dependency>

    <!-- SpringBoot-整合Web组件 -->

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-web</artifactId>

    </dependency>

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-data-redis</artifactId>

    </dependency>

    <!--mysql数据库驱动-->

    <dependency>

        <groupId>mysql</groupId>

        <artifactId>mysql-connector-java</artifactId>

    </dependency>

    <dependency>

        <groupId>org.mybatis.spring.boot</groupId>

        <artifactId>mybatis-spring-boot-starter</artifactId>

        <version>2.1.0</version>

    </dependency>

    <!-- 布隆过滤器 -->

    <dependency>

        <groupId>com.google.guava</groupId>

        <artifactId>guava</artifactId>

        <version>20.0</version>

    </dependency>

</dependencies>

<!-- 管理依赖 -->

<dependencyManagement>

    <dependencies>

        <dependency>

            <groupId>org.springframework.cloud</groupId>

            <artifactId>spring-cloud-dependencies</artifactId>

            <version>Finchley.M7</version>

            <type>pom</type>

            <scope>import</scope>

        </dependency>

    </dependencies>

</dependencyManagement>

application.yml

spring:

    redis:

        host: 127.0.0.1

        port:6379

        password: 123456

    database:1

      datasource:

        driver-class-name: com.mysql.jdbc.Driver

        url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&useSSL=false

        username: root

        password: root

RedisTemplateUtils.java

@Component

public class RedisTemplateUtils {

    @Resource

    private RedisTemplateredisTemplate;

        public void set(K k,V v){

        set(k, v,null);

    }

    public void set(K k,V v, Long timeout){

        redisTemplate.opsForValue().set(k, v);

        if(timeout !=null){

            redisTemplate.expire(k, timeout, TimeUnit.SECONDS);

        }

    }

    public V get(K k){

        return redisTemplate.opsForValue().get(k);

    }

}

UserEntity.java

@Data

public class UserEntityimplements Serializable {

    private int userId;

    private StringuserName;

}

UserMapper.java

public interface UserMapper {

    @Select("select userId, userName from user_t where userId=#{userId}")

    UserEntity getUser(int userId);

    @Select("select userId from user_t ")

    List getUserIds();

}

BloomFilterInit.java 当Spring启动后初始化布隆过滤器

@Component

public class BloomFilterInit implements ApplicationRunner {

    private BloomFilterbloomFilter;

    @Autowired

    private UserMapperuserMapper;

    @Override

    public void run(ApplicationArguments args)throws Exception {

        List userIds =userMapper.getUserIds();

        if (userIds.size() >0) {

            // 0.01即错误率为1%

            bloomFilter = BloomFilter.create(Funnels.integerFunnel(), userIds.size(),0.01);

            for (int i =0; i < userIds.size(); i++) {

                bloomFilter.put(userIds.get(i));

            }

            System.out.println("预热userId到布隆过滤器成功!");

        }

    }

    public BloomFilter getIntegerBloomFilter() {

        return bloomFilter;

    }

}

UserController.java

@RestController

public class UserController {

    @Autowired

    private UserMapperuserMapper;

    @Autowired

    private RedisTemplateUtilsredisTemplateUtils;

    @Autowired

    private BloomFilterInitbloomFilterInit;

    @RequestMapping("/getUser")

    public UserEntity getUser(int userId){

        if(!bloomFilterInit.getIntegerBloomFilter().mightContain(userId)){

            System.out.println("该userId在布隆过滤器中不存在!");

            return null;

        }

        UserEntity userEntity =redisTemplateUtils.get(userId +"");

        if(userEntity !=null){

            System.out.println("从Redis中返回数据!");

            return userEntity;

        }

        System.out.println("从数据库中查询数据!");

        UserEntity user =userMapper.getUser(userId);

        if(user !=null){

            System.out.println("将数据缓存到Redis中!");

            redisTemplateUtils.set(userId+"", user);

        }

        return user;

    }

}

App.java

@SpringBootApplication

@MapperScan("com.ttcv.bloom.mapper")

public class App {

    public static void main(String[] args) {

        SpringApplication.run(App.class);

    }

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容