1. 程式人生 > 資料庫 >redis布隆過濾器

redis布隆過濾器

布隆過濾器

定義

布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進位制向量和一系列隨機對映函式。

用處

布隆過濾器可以用於檢索一個元素是否在一個集合中。具體使用有:

  1. 網頁爬蟲對URL的去重,避免爬取相同的URL地址

  2. 反垃圾郵件,從數十億個垃圾郵件列表中判斷某郵箱是否垃圾郵箱(同理,垃圾簡訊)

  3. 快取穿透,將所有可能存在的資料快取放到布隆過濾器中,當黑客訪問不存在的快取時迅速返回避免快取及DB掛掉

關於快取穿透:

我們平時為了優化業務查詢效率,通常會選擇諸如redis一類的快取,資料查詢時如果快取有則直接通過快取拿取,沒有或者key過期的話,則去找資料庫. 找到之後再把資料加入到快取. 如果有這樣的一個場景,有使用者大量請求不存在的資料id,這個時候,因為快取

沒有,則統統全甩個數據庫,這樣很可能導致資料庫宕掉.同時資料全都直接由持久層獲得, 快取命中率引數失去了意義,快取也失去了意義.這類情況,稱之為快取穿透.

優點

它的優點是空間效率和查詢時間都比一般的演算法要好的多

缺點

它的缺點是有一定的誤識別率和刪除困難,但是瑕不掩瑜,他的優點足以讓我們選擇它作為提高查詢效能的工具.

原理

布隆過濾器內部維護一個全為0的bit陣列,需要說明的是,布隆過濾器有一個誤判率的概念,誤判率越低,則陣列越長,所佔空間越大。誤判率越高則陣列越小,所佔的空間越小。

因為是bit陣列,不是0 就是 1,這裡我們初始化一個16位全0的陣列:

這裡為簡化情況便於理解,我們設定hash函式個數為3,分別為 hash1(),hash2(),hash3()

bit陣列長度arrLength為16位

對資料 data1,分別使用 三個函式對其進行hash,這裡舉例hash1(),其他兩個都是相同的

hashX(data1),通過hash演算法和二進位制操作,然後  處理後的雜湊值 % arrLentgh,得到在陣列的下標 ,假設 下標 = 3,

如圖我們將陣列下標置為1:

同理,假設 3個函式處理完後如下圖:

這樣,花費很少的空間,就能夠儲存這條資料的存在情況,當同樣的資料請求過來,因為hash函式的特性,三個函式hash過後,

通過判斷三個位元位是否都是1,就可知道是否是同一條資料(???)

那麼,情況真的這麼簡單嗎?

其實,布隆過濾器有這樣一個特性,那就是: 如果所有位都重複不代表是重複資料,如果有哪怕一位不重複,則肯定不是重複資料

因為hash值相同,不一定是相同資料,這個好理解吧?

而hash值不同,肯定不是相同資料. 因此,我們知道,布隆過濾器對於是否重複的判斷,是有著誤判率的.這一點我們需要了解.

實現

實現方式1:  谷歌guaua框架(這方面請讀者自行百度一下)

實現方式2: 藉助redis

程式碼如下:

package com.example.demo.test;

import com.google.common.hash.Funnels;
import com.google.common.hash.Hashing;
import lombok.AllArgsConstructor;
import lombok.Data;
import redis.clients.jedis.Jedis;

import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;


/**
 * redis布隆過濾器 (布隆過濾器規則: 如果所有位都重複不代表是重複資料,則肯定不是重複資料)
 * <p>
 * 新增資料處理後id填充布隆過濾器(得HASH,設定bitmap的位) ->
 * 當新的請求來對比id,看看是不是能在布隆過濾器中找到重複資料 ->
 * true:判定為重複資料則進快取找,如果沒有,則是系統誤判,此時進入資料庫
 * false: 判定為非重複資料則直接進資料庫
 */

public class RedisBloomFilter {
    static final int expectedInsertions = 100;//要插入多少資料
    static final double fpp = 0.01;//期望的誤判率

    //bit陣列長度
    private static long numBits;

    //hash函式數量
    private static int numHashFunctions;

    private static Jedis jedis = new Jedis("127.0.0.1",6379);
    private static Map<String,Object> map = new HashMap<>();
    static {
        numBits = optimalNumOfBits(expectedInsertions,fpp);
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions,numBits);
        //資料模擬0(物件,需要用到序列化知識,篇幅過長,大家自己嘗試一下)
        //map.put("10000001",new Goods("10000001","雕牌洗衣粉",6.25,"洗衣粉" ));
        //map.put("10000002",new Goods("10000002","小米空調",3006,"小米空調" ));
        //map.put("10000003",new Goods("10000003","任天堂switch",1776.99,"任天堂switch" ));
        //map.put("10000004",new Goods("10000004","聯想膝上型電腦",6799,"聯想膝上型電腦" ));

        //資料模擬1(這裡只快取價格)
        map.put("10000001",6.25);
        map.put("10000002",3006);
        map.put("10000003",1776.99);
        map.put("10000004",6799);
    }

    public static void main(String[] args) {
        
        //模擬入快取的資料
        map.forEach((k,v)->{
            jedis.set(k,String.valueOf(v));
            long[] indexs = getIndexs(String.valueOf(k));
            for (long index : indexs) {
                jedis.setbit("codebear:bloom",index,true);
            }

        });
        
        //模擬使用者請求的資料
        String userInput1 = "10000001";
        String userInput2 = "10000005";
        String[] arr = {userInput1,userInput2};
        for (int j = 0; j < arr.length; j++) {
            boolean repeated = true;
            long[] indexs = getIndexs(String.valueOf(arr[j]));
            for (long index : indexs) {
                Boolean isContain = jedis.getbit("codebear:bloom",index);
                if (!isContain) {
                    System.out.println(arr[j] + "肯定沒有重複!");
                    repeated = false;
                    //從資料庫獲取資料
                    String retVal = getByDb(arr[j]);
                    System.out.println("資料庫獲取到的資料為"+retVal);
                    break;
                }
            }
            if (repeated) {
                System.out.println(arr[j] + "有重複!");
                //嘗試從快取獲取
                String retVal = getByCache(arr[j]);
                if (retVal == null) {
                    //從資料庫獲取
                    retVal = getByDb(arr[j]);
                    System.out.println("資料庫獲取到的資料為"+retVal);
                    break;

                }
                System.out.println("快取獲取到的資料為"+retVal);

            }
        }
        
    }


    /**
     * 從快取獲取資料
     */
    public static String getByCache(String key){
        return jedis.get(key);
    }

    /**
     * 從資料庫獲取資料
     */
    public static String getByDb(String key){
        //從資料庫獲取資料邏輯沒有實現
        return "";
    }

    /**
     * 根據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)));
    }
}