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

如何判断一个元素是不是在一个集合里

一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(哈希表)等等数据结构都是这种思路,但是随着集合中元素的增加,需要的存储空间越来越大;同时检索速度也越来越慢,检索时间复杂度分别是O(n)、O(log n)、O(1)
为了解决存储空间和检索速度慢的问题,可以使用布隆过滤器数据结构。

什么是布隆过滤器

BloomFilter是一种空间效率极高的概率型算法和数据结构,它可以用来判断某个元素是否在集合内,具有运行快速,内存占用小的特点。
而高效插入和查询的代价就是,BloomFilter 是一个基于概率的数据结构:它只能告诉我们一个元素绝对不在集合内或可能在集合内。
BloomFilter 的基础数据结构是一个很长的二进制向量(可理解为数组,并且只能存放0、1),还有一系列Hash函数计算元素的位置。

Hash函数:SHA1、SHA256、MD5...
在这里插入图片描述

布隆过滤器的原理

初始化一个位数组,当一个元素X被加入集合时,通过 M 个散列函数将这个元素映射成一个位数组(Bit array)中的 N 个点,把N个节点设置为 1 。检索时,只要看看这些点是不是都是1就知道元素是否在集合中;如果这些点有任何一个 0,则被检元素一定不在;如果都是1,则被检元素很可能在(之所以说可能是误差的存在)。因为不同元素通过散列函数得出的节点是有重复的,所以可能存在不存在的元素所有的节点也是1。

布隆过滤器应用的经典场景

  • 垃圾邮件地址过滤
  • 解决缓存穿透问题

布隆过滤器优势和劣势

  • 优势:
    全量存储但是不存储元素本身,在某些对保密要求非常严格的场合有优势;
    空间高效率;
    插入/查询时间都是常数O(k),远远超过一般的算法;
  • 劣势:
    存在误算率,随着存入的元素数量增加,误算率随之增加;
    一般情况下不能从布隆过滤器中删除元素;
    数组长度以及hash函数个数确定过程复杂;

使用GooleGuava实现BloomFilter

BloomFilter流程:

  • 首先需要 k 个 hash 函数,每个函数可以把 key 散列成为 1 个整数;
  • 初始化时,需要一个长度为 n 比特的数组,每个比特位初始化为 0;
  • 某个 key 加入集合时,用 k 个 hash 函数计算出 k 个散列值,并把数组中对应的比特位置为 1;
  • 判断某个 key 是否在集合时,用 k 个 hash 函数计算出 k 个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为可能存在集合中。只要有一位是 0 ,认为一定不存在集合中。

BloomFilter实战:
场景描述:
100w字符串放入布隆过滤器,另外随机生成1w字符串,判断他们在100w里面是否存在。
了解误判率对hash函数个数以及bit数组长度的影响。
使用BloomFilter解决缓存击穿的问题。

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.junit.Test;

import java.util.*;

public class BloomFilterTest {

    private static final int insertions = 1000000; // 100w

    @Test
    public void bfTest() {
        // 初始化一个存储string数据的布隆过滤器,初始化大小100w,不能设置为0
        BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions, 0.001);
        // 初始化一个存储string数据的set,初始化大小100w
        Set<String> sets = new HashSet<>(insertions);
        // 初始化一个存储string数据的set,初始化大小100w
        List<String> lists = new ArrayList<String>(insertions);

        // 向三个容器初始化100万个随机并且唯一的字符串---初始化操作
        for (int i = 0; i < insertions; i++) {
            String uuid = UUID.randomUUID().toString();
            bf.put(uuid);
            sets.add(uuid);
            lists.add(uuid);
        }

        int wrong = 0;//布隆过滤器错误判断的次数
        int right = 0;//布隆过滤器正确判断的次数
        for (int i = 0; i < 10000; i++) {
            String test = i % 100 == 0 ? lists.get(i / 100) : UUID.randomUUID().toString();//按照一定比例选择bf中肯定存在的字符串
            if (bf.mightContain(test)) {
                if (sets.contains(test)) {
                    right++;
                } else {
                    wrong++;
                }
            }
        }
        System.out.println("=================right=====================" + right);//100
        System.out.println("=================wrong=====================" + wrong);//10
    }
}

布隆过滤器落地场景是:优化关联查询
优化背景: 查询订单需要关联预警订单数据,由于每查询一笔预警数据就要查询一次预警表,效率低,即是判断该订单是否预警。
解决方案: 可以先将预警的订单放到布隆过滤器中存放一份,则查询订单的时候可以用于关联。
应用该场景的原因: 大部分订单还是正常的,所以没必要每次去关联预警表。
先去布隆过滤器查询该订单是否存在,不存在则直接返回正常,存在则去预警表查询,允许一定的误差率。
为什么预警的订单在布隆过滤器中存放一份也需要去预警表查询?
因为判断了存在的数据可能会有一定的误差率,在布隆过滤器中存在,不一定在预警表是存在的。

使用BitSet实现一个简单的布隆过滤器

import java.util.Arrays;
import java.util.BitSet;

public class MyBloomFilter {

    // 布隆过滤器容量
    private static final int DEFAULT_SIZE = 2 << 28;
    // bit数组,用来存放结果
    private static BitSet bitSet = new BitSet(DEFAULT_SIZE);
    // 后面hash函数会用到,用来生成不同的hash值,可随意设置
    private static final int[] hashInts = {1, 6, 16, 38, 58, 68};

    // add方法,计算出key的hash值,并将对应下标置为true
    public void add(Object key) {
        Arrays.stream(hashInts).forEach(i -> bitSet.set(hash(key, i)));
    }

    // 判断key是否存在,true不一定说明key存在,但是false一定说明不存在
    public boolean isContain(Object key) {
        boolean result = true;
        for (int i : hashInts) {
            // 短路与,只要有一个bit位为false,则返回false
            result = result && bitSet.get(hash(key, i));
        }
        return result;
    }

    // hash函数,借鉴了hashmap的扰动算法
    private int hash(Object key, int i) {
        int h;
        return key == null ? 0 : (i * (DEFAULT_SIZE - 1) & ((h = key.hashCode()) ^ (h >>> 16)));
    }

    public static void main(String[] args) {
        MyBloomFilter myNewBloomFilter = new MyBloomFilter();
        myNewBloomFilter.add("James");
        myNewBloomFilter.add("Allen");
        myNewBloomFilter.add("Kobe");
        System.out.println(myNewBloomFilter.isContain("James"));//true
        System.out.println(myNewBloomFilter.isContain("James "));//false
        System.out.println(myNewBloomFilter.isContain("James1"));//false
        System.out.println(myNewBloomFilter.isContain("Allen"));//true
        System.out.println(myNewBloomFilter.isContain("Kobe123"));//false
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值