Redis 技術內幕——布隆過濾器

現有 50 億個電話號碼,如何快速準確判斷某 10 萬個電話號碼是否在這 50 億中。

如果通過數據庫查詢實現會非常慢。
如果數據預放在集合中,50 億 x 8 字節,大概需要 40 GB內存,內存浪費或不夠。
如果使用 hyperloglog 存儲,可以使用很小的內存判斷數據是否在 hyperloglog 裏,但是結果會不準確。

類似問題還有很多,例如垃圾郵件過濾、文字處理軟件(例如 Word)錯誤單詞檢測、網絡爬蟲重複 url 檢測、Hbase 行過濾等。

基於這類問題,就引出了布隆過濾器。

布隆過濾器是 1970 年伯頓.布隆提出,用很小的空間,解決上述類似問題。

1.布隆過濾器原理

實現原理:一個很長的二進制向量和若干個哈希函數。

布隆過濾器是一個 bit 向量或者說 bit 數組,長這樣:

Redis 布隆過濾器原理1

如果我們要映射一個值到布隆過濾器中,我們需要使用多個不同的哈希函數生成多個哈希值,並對每個生成的哈希值指向的 bit 位置 1,例如針對值 “geeks” 和三個不同的哈希函數分別生成了哈希值 1、4、7,則上圖轉變爲:

Redis 布隆過濾器原理2

我們現在再存一個值 “nerd”,如果哈希函數返回 3、4、5 的話,圖繼續變爲:

Redis 布隆過濾器原理3

值得注意的是,4 這個 bit 位由於兩個值的哈希函數都返回了這個 bit 位,因此它被覆蓋了。

m 個二進制向量,n 個預備數據,k 個 hash 函數。構建布隆過濾器:n 個預備數據走一遍上面過程。判斷元素是否存在:走一遍上面過程,如果都是 1,則表明存在,反之不存在。

2.布隆過濾器誤差率

誤差是肯定存在的,跟 m/n 的比率和 hash 函數的個數有關係,m/n 的比率越大,誤差越低,hash 函數的個數越多,誤差越低。

1 個元素,1 個 hash 函數,任意一個比特爲 1 的概率爲 1/m1/m,依然爲 0 的概率爲 1(1/m)1- (1/m)
1 個元素,k 個 hash 函數,依然爲 0 的概率爲 (1(1/m))k(1- (1/m))^{k}
n 個元素,k 個 hash 函數,依然爲 0 的概率爲 (1(1/m))nk(1- (1/m))^{nk},被設置爲 1 的概率爲 1(1(1/m))nk1 - (1- (1/m))^{nk}
新元素全中的概率爲 (1(1(1/m))nk)k(1 - (1- (1/m))^{nk})^{k}

3.本地布隆過濾器

現有庫:guava。使用方法非常簡單:

BloomFilter<Person> friends = BloomFilter.create(personFunnel, 500, 0.01); // 500:希望插入的個數, 0.01:期望的誤差率
for(Person friend : friendsList) {
    friends.put(friend);
}

// 很久以後
if (friends.mightContain(dude)) {
    //dude不是朋友還運行到這裏的概率爲1%
    //在這兒,我們可以在做進一步精確檢查的同時觸發一些異步加載
}

本地布隆過濾器的問題:

  • 容量受限制;
  • 多個應用存在多個布隆過濾器,構建同步複雜;

4.Redis單機布隆過濾器

基於位圖實現。首先定義布隆過濾器接口:

import java.util.List;
import java.util.Map;
/**
 * 布隆過濾器接口
 */
public interface BloomFilter<T> {
    /**
     * 添加
     * @param object
     * @return 是否添加成功
     */
    boolean add(T object);
    /**
     * 批量添加
     * @param objectList
     * @return 是否添加成功
     */
    Map<T, Boolean> batchAdd(List<T> objectList);
    /**
     * 是否包含
     * @param object
     */
    boolean contains(T object);
    /**
     * 是否包含
     * @param object
     */
    Map<T, Boolean> batchContains(List<T> objectList);
    /**
     * 刪除
     */
    void clear();
    /**
     * 預期插入數量
     */
    long getExpectedInsertions();
    /**
     * 預期錯誤概率
     */
    double getFalseProbability();
    /**
     * 布隆過濾器總長度
     */
    long getSize();
    /**
     * hash函數迭代次數
     */
    int getHashIterations();
}

CacheCloud 布隆過濾器實現 BloomFilter 接口,看一下 add 方法的實現:

/**
 * CacheCloud 布隆過濾器
 */
public class CacheCloudBloomFilter<T> implements BloomFilter<T> {
    private BloomFilterBuilder config;
    public CacheCloudBloomFilter(BloomFilterBuilder bloomFilterBuilder) {
        this.config = bloomFilterBuilder;
    }

    @Override
    public boolean add(T object) {
        if (object == null) {
            return false;
        }
        // 偏移量列表
        List<Integer> offsetList = hash(object);
        if (offsetList == null || offsetList.isEmpty()) {
            return false;
        }
        String key = genBloomFilterDistributeKey(object);
        return setBit(key, new HashSet<Integer>(offsetList));
    }
    
    /**
     * 生成子布隆過濾器對應的key,使用crc16作爲分組
     */
    private String genBloomFilterDistributeKey(T object) {
        int hashcode = JedisClusterCRC16.getCRC16(object.toString());
        int segement = hashcode % (getConfig().getBloomNumber() + 1);
        return getBloomFilterKey(segement);
    }
    /**
     * 獲取布隆過濾器key
     */
    private String getBloomFilterKey(int index) {
        return getName() + ":" + index;
    }
    public String getName() {
        return getConfig().getName();
    }
    public BloomFilterBuilder getConfig() {
        return config;
    }
    public HashFunction getHashFunction() {
        return getConfig().getHashFunction();
    }
    public List<Integer> hash(Object object) {
        byte[] bytes = object.toString().getBytes();
        return getHashFunction().hash(bytes, getConfig().getBloomMaxSize(), getConfig().getHashIterations());
    }
}

BloomFilterBuilder 是布隆過濾器的構造器,裏面包含了很多參數,包括 JedisPool、布隆過濾器名 name、大位圖總長度 totalSize、每個小位圖(布隆過濾器)長度 bloomMaxSize、布隆過濾器個數 bloomNumber、hash函數個數 hashIterations、預期插入條數 expectedInsertions、預期錯誤概率 falseProbability、hash函數 hashFunction、是否重寫已經存在的布隆過濾器 overwriteIfExists、是否完成 done。

基於 Redis 單機實現存在的問題:

  • 速度慢,比本地慢,可以考慮與應用同機房部署;
  • 容量受限,Redis 最大字符串爲 512 MB,Redis 單機容量受限,可以考慮基於 Redis Cluster 實現;

5.Redis分佈式布隆過濾器

基於 Redis Cluster 實現。會使用多個布隆過濾器,二次路由。

基於 pipeline 提高效率:

/**
 * pipeline setbit
 */
private boolean pipelineSetBit(String key, Set<Integer> offsetSet) {
    int slot = JedisClusterCRC16.getSlot(key);
    JedisPool jedisPool = getJedisCluster().getConnectionHandler().getJedisPoolFromSlot(slot);
    Jedis jedis = null;
    Pipeline pipeline = null;
    try {
        jedis = jedisPool.getResource();
        pipeline = jedis.pipelined();
        for (int offset : offsetSet) {
            pipeline.setbit(key, offset, true);
        }
        pipeline.sync();
        return true;
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
        return false;
    } finally {
        if (pipeline != null)
            pipeline.clear();
        if (jedis != null)
            jedis.close();
    }
}
/**
 * pipeline get
 */
private Map<Integer, Boolean> pipelineGetBit(String key, List<Integer> offsetList) {
    Map<Integer, Boolean> offsetResultMap = new HashMap<Integer, Boolean>();
    int slot = JedisClusterCRC16.getSlot(key);
    JedisPool jedisPool = getJedisCluster().getConnectionHandler().getJedisPoolFromSlot(slot);
    Jedis jedis = null;
    Pipeline pipeline = null;
    try {
        jedis = jedisPool.getResource();
        pipeline = jedis.pipelined();
        for (int offset : offsetList) {
            pipeline.getbit(key, offset);
        }
        List<Object> objectList = pipeline.syncAndReturnAll();
        int i = 0;
        for (Object object : objectList) {
            offsetResultMap.put(offsetList.get(i), (Boolean) object);
            i++;
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    } finally {
        if (pipeline != null)
            pipeline.clear();
        if (jedis != null)
            jedis.close();
    }
    return offsetResultMap;
}

參考:
https://hackernoon.com/probabilistic-data-structures-bloom-filter-5374112a7832
https://www.jasondavies.com/bloomfilter/
http://ifeve.com/google-guava-hashing/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章