摘要:方法,即表示自動(dòng)擴(kuò)容,這個(gè)函數(shù)名是從中搬過(guò)來(lái)的。自動(dòng)擴(kuò)容最后,也是最重要的,其中用了布隆過(guò)濾器中計(jì)算型數(shù)組長(zhǎng)度的方法,得到之后使用命令初始化一個(gè)全部為的位數(shù)組。
本文源地址:http://www.fullstackyang.com/...,轉(zhuǎn)發(fā)請(qǐng)注明該地址或segmentfault地址,謝謝!一、背景知識(shí)
在網(wǎng)上已經(jīng)有很多關(guān)于布隆過(guò)濾器的介紹了,這里就不再贅述,下面簡(jiǎn)單地提煉幾個(gè)要點(diǎn):
布隆過(guò)濾器是用來(lái)判斷一個(gè)元素是否出現(xiàn)在給定集合中的重要工具,具有快速,比哈希表更節(jié)省空間等優(yōu)點(diǎn),而缺點(diǎn)在于有一定的誤識(shí)別率(false-positive,假陽(yáng)性),亦即,它可能會(huì)把不是集合內(nèi)的元素判定為存在于集合內(nèi),不過(guò)這樣的概率相當(dāng)小,在大部分的生產(chǎn)環(huán)境中是可以接受的;
其原理比較簡(jiǎn)單,如下圖所示,S集合中有n個(gè)元素,利用k個(gè)哈希函數(shù),將S中的每個(gè)元素映射到一個(gè)長(zhǎng)度為m的位(bit)數(shù)組B中不同的位置上,這些位置上的二進(jìn)制數(shù)均置為1,如果待檢測(cè)的元素經(jīng)過(guò)這k個(gè)哈希函數(shù)的映射后,發(fā)現(xiàn)其k個(gè)位置上的二進(jìn)制數(shù)不全是1,那么這個(gè)元素一定不在集合S中,反之,該元素可能是S中的某一個(gè)元素(參考1);
綜上描述,那么到底需要多少個(gè)哈希函數(shù),以及創(chuàng)建長(zhǎng)度為多少的bit數(shù)組比較合適,為了估算出k和m的值,在構(gòu)造一個(gè)布隆過(guò)濾器時(shí),需要傳入兩個(gè)參數(shù),即可以接受的誤判率fpp和元素總個(gè)數(shù)n(不一定完全精確)。至于參數(shù)估計(jì)的方法,有興趣的同學(xué)可以參考維基英文頁(yè)面,下面直接給出公式:
哈希函數(shù)的要求盡量滿足平均分布,這樣既降低誤判發(fā)生的概率,又可以充分利用bit數(shù)組的空間;
根據(jù)論文《Less Hashing, Same Performance: Building a Better Bloom Filter》提出的一個(gè)技巧,可以用2個(gè)哈希函數(shù)來(lái)模擬k個(gè)哈希函數(shù),即gi(x) = h1(x) + ih2(x) ,其中0<=i<=k-1;
在吳軍博士的《數(shù)學(xué)之美》一書(shū)中展示了不同情況下的誤判率,例如,假定一個(gè)元素用16位比特,8個(gè)哈希函數(shù),那么假陽(yáng)性的概率是萬(wàn)分之五,這已經(jīng)相當(dāng)小了。
目前已經(jīng)有相應(yīng)實(shí)現(xiàn)的開(kāi)源類庫(kù),如Google的Guava類庫(kù),Twitter的Algebird類庫(kù),和ScalaNLP breeze等等,其中Guava 11.0版本中增加了BloomFilter類,它使用了Funnel和Sink的設(shè)計(jì),增強(qiáng)了泛化的能力,使其可以支持任何數(shù)據(jù)類型,其利用murmur3 hash來(lái)做哈希映射函數(shù),不過(guò)它底層并沒(méi)有使用傳統(tǒng)的java.util.BitSet來(lái)做bit數(shù)組,而是用long型數(shù)組進(jìn)行了重新封裝,大部分操作均基于位的運(yùn)算,因此能達(dá)到一個(gè)非常好的性能;下面我們就Guava類庫(kù)中實(shí)現(xiàn)布隆過(guò)濾器的源碼作詳細(xì)分析,最后出于靈活性和解耦等因素的考慮,我們想要把布隆過(guò)濾器從JVM中拿出來(lái),于是利用了Redis自帶的Bitmaps作為底層的bit數(shù)組進(jìn)行重構(gòu),另外隨著插入的元素越來(lái)越多,當(dāng)實(shí)際數(shù)量遠(yuǎn)遠(yuǎn)大于創(chuàng)建時(shí)設(shè)置的預(yù)計(jì)數(shù)量時(shí),布隆過(guò)濾器的誤判率會(huì)越來(lái)越高,因此在重構(gòu)的過(guò)程中增加了自動(dòng)擴(kuò)容的特性,最后通過(guò)測(cè)試驗(yàn)證其正確性。
二、布隆過(guò)濾器在Guava中的實(shí)現(xiàn)Guava中,布隆過(guò)濾器的實(shí)現(xiàn)主要涉及到2個(gè)類,BloomFilter和BloomFilterStrategies,首先來(lái)看一下BloomFilter:
/** The bit set of the BloomFilter (not necessarily power of 2!) */ private final BitArray bits; /** Number of hashes per element */ private final int numHashFunctions; /** The funnel to translate Ts to bytes */ private final Funnel super T> funnel; /** * The strategy we employ to map an element T to {@code numHashFunctions} bit indexes. */ private final Strategy strategy;
這是它的4個(gè)成員變量:
BitArrays是定義在BloomFilterStrategies中的內(nèi)部類,封裝了布隆過(guò)濾器底層bit數(shù)組的操作,后文詳述;
numHashFunctions表示哈希函數(shù)的個(gè)數(shù),即上文提到的k;
Funnel,這是Guava中定義的一個(gè)接口,它和PrimitiveSink配套使用,主要是把任意類型的數(shù)據(jù)轉(zhuǎn)化成Java基本數(shù)據(jù)類型(primitive value,如char,byte,int……),默認(rèn)用java.nio.ByteBuffer實(shí)現(xiàn),最終均轉(zhuǎn)化為byte數(shù)組;
Strategy是定義在BloomFilter類內(nèi)部的接口,代碼如下,有3個(gè)方法,put(插入元素),mightContain(判定元素是否存在)和ordinal方法(可以理解為枚舉類中那個(gè)默認(rèn)方法)
interface Strategy extends java.io.Serializable { /** * Sets {@code numHashFunctions} bits of the given bit array, by hashing a user element. * *Returns whether any bits changed as a result of this operation. */
boolean put(T object, Funnel super T> funnel, int numHashFunctions, BitArray bits); /** * Queries {@code numHashFunctions} bits of the given bit array, by hashing a user element; * returns {@code true} if and only if all selected bits are set. */ boolean mightContain( T object, Funnel super T> funnel, int numHashFunctions, BitArray bits); /** * Identifier used to encode this strategy, when marshalled as part of a BloomFilter. Only * values in the [-128, 127] range are valid for the compact serial form. Non-negative values * are reserved for enums defined in BloomFilterStrategies; negative values are reserved for any * custom, stateful strategy we may define (e.g. any kind of strategy that would depend on user * input). */ int ordinal(); }
對(duì)于創(chuàng)建布隆過(guò)濾器,BloomFilter并沒(méi)有公有的構(gòu)造函數(shù),只有一個(gè)私有構(gòu)造函數(shù),而對(duì)外它提供了5個(gè)重載的create方法,在缺省情況下誤判率設(shè)定為3%,采用BloomFilterStrategies.MURMUR128_MITZ_64的實(shí)現(xiàn)。其中4個(gè)create方法最終都調(diào)用了同一個(gè)create方法,由它來(lái)負(fù)責(zé)調(diào)用私有構(gòu)造函數(shù),其源碼如下:
staticBloomFilter create( Funnel super T> funnel, long expectedInsertions, double fpp, Strategy strategy) { checkNotNull(funnel); checkArgument( expectedInsertions >= 0, "Expected insertions (%s) must be >= 0", expectedInsertions); checkArgument(fpp > 0.0, "False positive probability (%s) must be > 0.0", fpp); checkArgument(fpp < 1.0, "False positive probability (%s) must be < 1.0", fpp); checkNotNull(strategy); if (expectedInsertions == 0) { expectedInsertions = 1; } /* * TODO(user): Put a warning in the javadoc about tiny fpp values, since the resulting size * is proportional to -log(p), but there is not much of a point after all, e.g. * optimalM(1000, 0.0000000000000001) = 76680 which is less than 10kb. Who cares! */ long numBits = optimalNumOfBits(expectedInsertions, fpp); int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits); try { return new BloomFilter (new BitArray(numBits), numHashFunctions, funnel, strategy); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e); } }
在create中接受了4個(gè)參數(shù),funnel(輸入的數(shù)據(jù)),expectedInsertions(預(yù)計(jì)插入的元素總數(shù)),fpp(期望誤判率),strategy(實(shí)現(xiàn)Strategy的實(shí)例),然后它計(jì)算了bit數(shù)組的長(zhǎng)度以及哈希函數(shù)的個(gè)數(shù)(公式參考前文),最后用numBits創(chuàng)建了BitArray,并調(diào)用了構(gòu)造函數(shù)完成賦值操作。
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))); } static int optimalNumOfHashFunctions(long n, long m) { // (m / n) * log(2), but avoid truncation due to division! return Math.max(1, (int) Math.round((double) m / n * Math.log(2))); }
接著再來(lái)看一下BloomFilterStrategies類,首先它是實(shí)現(xiàn)了BloomFilter.Strategy 接口的一個(gè)枚舉類,其次它有兩個(gè)2枚舉值,MURMUR128_MITZ_32和MURMUR128_MITZ_64,分別對(duì)應(yīng)了32位哈希映射函數(shù),和64位哈希映射函數(shù),后者使用了murmur3 hash生成的所有128位,具有更大的空間,不過(guò)原理是相通的,我們選擇默認(rèn)的MURMUR128_MITZ_64來(lái)分析:
MURMUR128_MITZ_64() { @Override publicboolean put( T object, Funnel super T> funnel, int numHashFunctions, BitArray bits) { long bitSize = bits.bitSize(); byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal(); long hash1 = lowerEight(bytes); long hash2 = upperEight(bytes); boolean bitsChanged = false; long combinedHash = hash1; for (int i = 0; i < numHashFunctions; i++) { // Make the combined hash positive and indexable bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize); combinedHash += hash2; } return bitsChanged; } @Override public boolean mightContain( T object, Funnel super T> funnel, int numHashFunctions, BitArray bits) { long bitSize = bits.bitSize(); byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal(); long hash1 = lowerEight(bytes); long hash2 = upperEight(bytes); long combinedHash = hash1; for (int i = 0; i < numHashFunctions; i++) { // Make the combined hash positive and indexable if (!bits.get((combinedHash & Long.MAX_VALUE) % bitSize)) { return false; } combinedHash += hash2; } return true; }
抽象來(lái)看,put是寫(xiě),mightContain是讀,兩個(gè)方法的代碼有一點(diǎn)相似,都是先利用murmur3 hash對(duì)輸入的funnel計(jì)算得到128位的字節(jié)數(shù)組,然后高低分別取8個(gè)字節(jié)(64位)創(chuàng)建2個(gè)long型整數(shù)hash1,hash2作為哈希值。循環(huán)體內(nèi)采用了2個(gè)函數(shù)模擬其他函數(shù)的思想,即上文提到的gi(x) = h1(x) + ih2(x) ,這相當(dāng)于每次累加hash2,然后通過(guò)基于bitSize取模的方式在bit數(shù)組中索引。
這里之所以要和Long.MAX_VALUE進(jìn)行按位與的操作,是因?yàn)樵诔龜?shù)和被除數(shù)符號(hào)不一致的情況下計(jì)算所得的結(jié)果是有差別的,在程序語(yǔ)言里,“%”準(zhǔn)確來(lái)說(shuō)是取余運(yùn)算(C,C++和Java均如此,python是取模),如-5%3=-2,而取模的數(shù)學(xué)定義是x
mod y=x-y[x/y](向下取整),所以-5 mod 3=
-5-3*(-2)=1,因此當(dāng)哈希值為負(fù)數(shù)的時(shí)候,其取余的結(jié)果為負(fù)(bitSize始終為正數(shù)),這樣就不方便在bit數(shù)組中取值,因此通過(guò)Long.MAX_VALUE(二進(jìn)制為0111…1111),直接將開(kāi)頭的符號(hào)位去掉,從而轉(zhuǎn)變?yōu)檎龜?shù)。當(dāng)然也可以取絕對(duì)值,在另一個(gè)MURMUR128_MITZ_32的實(shí)現(xiàn)中就是這么做的。
在put方法中,先是將索引位置上的二進(jìn)制置為1,然后用bitsChanged記錄插入結(jié)果,如果返回true表明沒(méi)有重復(fù)插入成功,而mightContain方法則是將索引位置上的數(shù)值取出,并判斷是否為0,只要其中出現(xiàn)一個(gè)0,那么立即判斷為不存在。
最后再說(shuō)一下底層bit數(shù)組的實(shí)現(xiàn),主要代碼如下:
static final class BitArray { final long[] data; long bitCount; BitArray(long bits) { this(new long[Ints.checkedCast(LongMath.divide(bits, 64, RoundingMode.CEILING))]); } // Used by serialization BitArray(long[] data) { checkArgument(data.length > 0, "data length is zero!"); this.data = data; long bitCount = 0; for (long value : data) { bitCount += Long.bitCount(value); } this.bitCount = bitCount; } /** Returns true if the bit changed value. */ boolean set(long index) { if (!get(index)) { data[(int) (index >>> 6)] |= (1L << index); bitCount++; return true; } return false; } boolean get(long index) { return (data[(int) (index >>> 6)] & (1L << index)) != 0; } /** Number of bits */ long bitSize() { return (long) data.length * Long.SIZE; } ... }
之前也提到了Guava沒(méi)有使用java.util.BitSet,而是封裝了一個(gè)long型的數(shù)組,另外還有一個(gè)long型整數(shù),用來(lái)統(tǒng)計(jì)數(shù)組中已經(jīng)占用(置為1)的數(shù)量,在第一個(gè)構(gòu)造函數(shù)中,它把傳入的long型整數(shù)按長(zhǎng)度64分段(例如129分為3段),段數(shù)作為數(shù)組的長(zhǎng)度,你可以想象成由若干個(gè)64位數(shù)組拼接成一個(gè)超長(zhǎng)的數(shù)組,它的長(zhǎng)度就是64乘以段數(shù),即bitSize,在第二個(gè)構(gòu)造函數(shù)中利用Long.bitCount方法來(lái)統(tǒng)計(jì)對(duì)應(yīng)二進(jìn)制編碼中的1個(gè)數(shù),這個(gè)方法在JDK1.5中就有了,其算法設(shè)計(jì)得非常精妙,有精力的同學(xué)可以自行研究。
另外兩個(gè)重要的方法是set和get,在get方法中,參考put和mightContain方法,傳入的參數(shù)index是經(jīng)過(guò)bitSize取模的,因此一定能落在這個(gè)超長(zhǎng)數(shù)組的范圍之內(nèi),為了獲取index對(duì)應(yīng)索引位置上的值,首先將其無(wú)符號(hào)右移6位,并且強(qiáng)制轉(zhuǎn)換成int型,這相當(dāng)于除以64向下取整的操作,也就是換算成段數(shù),得到該段上的數(shù)值之后,又將1左移index位,最后進(jìn)行按位與的操作,如果結(jié)果等于0,那么返回false,從而在mightContain中判斷為不存在。在set方法中,首先調(diào)用了get方法判斷是否已經(jīng)存在,如果不存在,則用同樣的邏輯取出data數(shù)組中對(duì)應(yīng)索引位置的數(shù)值,然后按位或并賦值回去。
到這里,對(duì)Guava中布隆過(guò)濾器的實(shí)現(xiàn)就基本討論完了,簡(jiǎn)單總結(jié)一下:
BloomFilter類的作用在于接收輸入,利用公式完成對(duì)參數(shù)的估算,最后初始化Strategy接口的實(shí)例;
BloomFilterStrategies是一個(gè)枚舉類,具有兩個(gè)實(shí)現(xiàn)了Strategy接口的成員,分別為MURMUR128_MITZ_32和MURMUR128_MITZ_64,另外封裝了long型的數(shù)組作為布隆過(guò)濾器底層的bit數(shù)組,其中在get和set方法中完成核心的位運(yùn)算。
三、利用Redis Bitmaps進(jìn)行重構(gòu)通過(guò)上面的分析,主要算法和邏輯的部分大體都是一樣的,真正需要重構(gòu)的部分是底層位數(shù)組的實(shí)現(xiàn),在Guava中是封裝了一個(gè)long型的數(shù)組,而對(duì)于redis來(lái)說(shuō),本身自帶了Bitmaps的“數(shù)據(jù)結(jié)構(gòu)”(本質(zhì)上還是一個(gè)字符串),已經(jīng)提供了位操作的接口,因此重構(gòu)本身并不復(fù)雜,相對(duì)比較復(fù)雜的是,之前提到的實(shí)現(xiàn)自動(dòng)擴(kuò)容特性。
這里實(shí)現(xiàn)自動(dòng)擴(kuò)容的思想是,在redis中記錄一個(gè)自增的游標(biāo)cursor,如果當(dāng)前key對(duì)應(yīng)的Bitmaps已經(jīng)達(dá)到飽和狀態(tài),則cursor自增,同時(shí)用其生成一個(gè)新的key,并創(chuàng)建規(guī)模同等的Bitmaps。然后在get的時(shí)候,需要判斷該元素是否存在于任意一個(gè)Bitmaps中。于是整個(gè)的邏輯就變成,一個(gè)元素在每個(gè)Bitmaps中都不存在時(shí),才能插入當(dāng)前cursor對(duì)應(yīng)key的Bitmaps中。
下面是代碼的實(shí)現(xiàn)部分。
首先,為了簡(jiǎn)化redis的操作,定義了2個(gè)函數(shù)式接口,分別執(zhí)行單條命令和pipeline,另外還實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的工具類
@FunctionalInterface public interface JedisExecutor{ T execute(Jedis jedis); } @FunctionalInterface public interface PipelineExecutor { void load(Pipeline pipeline); } public class JedisUtils { private static final GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); private JedisPool jedisPool; public JedisUtils() { jedisPool = new JedisPool(poolConfig, "localhost", 6379); } public T execute(JedisExecutor jedisExecutor) { try (Jedis jedis = jedisPool.getResource()) { return jedisExecutor.execute(jedis); } } public List
其次在Strategy中,對(duì)put和mightContain作了一點(diǎn)修改,其中被注釋的部分是Guava中的實(shí)現(xiàn)。為了簡(jiǎn)化,這里我們只接受String對(duì)象。
這里先把所有的隨機(jī)函數(shù)對(duì)應(yīng)的索引位置收集到一個(gè)數(shù)組中,然后交由底層的RedisBitmaps處理get或set,具體過(guò)程后面會(huì)詳細(xì)說(shuō)明。
bits.ensureCapacityInternal()方法,即表示自動(dòng)擴(kuò)容,這個(gè)函數(shù)名是從ArrayList中搬過(guò)來(lái)的。
@Override public boolean put(String string, int numHashFunctions, RedisBitmaps bits) { long bitSize = bits.bitSize(); byte[] bytes = Hashing.murmur3_128().hashString(string, Charsets.UTF_8).asBytes(); long hash1 = lowerEight(bytes); long hash2 = upperEight(bytes); boolean bitsChanged = false; long combinedHash = hash1; // for (int i = 0; i < numHashFunctions; i++) { // bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize); // combinedHash += hash2; // } long[] offsets = new long[numHashFunctions]; for (int i = 0; i < numHashFunctions; i++) { offsets[i] = (combinedHash & Long.MAX_VALUE) % bitSize; combinedHash += hash2; } bitsChanged = bits.set(offsets); bits.ensureCapacityInternal();//自動(dòng)擴(kuò)容 return bitsChanged; } @Override public boolean mightContain(String object, int numHashFunctions, RedisBitmaps bits) { long bitSize = bits.bitSize(); byte[] bytes = Hashing.murmur3_128().hashString(object, Charsets.UTF_8).asBytes(); long hash1 = lowerEight(bytes); long hash2 = upperEight(bytes); long combinedHash = hash1; // for (int i = 0; i < numHashFunctions; i++) { // if (!bits.get((combinedHash & Long.MAX_VALUE) % bitSize)) { // return false; // } // combinedHash += hash2; // } // return true; long[] offsets = new long[numHashFunctions]; for (int i = 0; i < numHashFunctions; i++) { offsets[i] = (combinedHash & Long.MAX_VALUE) % bitSize; combinedHash += hash2; } return bits.get(offsets); }
最后,也是最重要的RedisBitmaps,其中bitSize用了Guava布隆過(guò)濾器中計(jì)算Long型數(shù)組長(zhǎng)度的方法,得到bitSize之后使用setbit命令初始化一個(gè)全部為0的位數(shù)組。get(long offset)和set(long offset),這兩個(gè)與Guava布隆過(guò)濾器中的邏輯類似,這里就不再贅述了,而get(long[] offsets)方法中,所有的offset要與每一個(gè)cursor對(duì)應(yīng)的Bitmaps進(jìn)行判斷,若全部命中,那么這個(gè)元素就可能存在于該Bitmaps,反之若不能完全命中,則表示該元素不存在于任何一個(gè)Bitmaps,所以當(dāng)滿足這個(gè)條件,在set(long[] offsets)方法中,就可以插入到當(dāng)前key的Bitmaps中了。
在ensureCapacityInternal方法,需要擴(kuò)容的判斷條件是bitCount*2>bitSize,bitCount表示一個(gè)Bitmaps中“1”出現(xiàn)的個(gè)數(shù),也就是當(dāng)“1”出現(xiàn)的個(gè)數(shù)超過(guò)總數(shù)的一半時(shí),進(jìn)行擴(kuò)容操作——首先使用incr命令對(duì)cursor自增,然后使用新的key創(chuàng)建一個(gè)新的Bitmaps。
RedisBitmapsJava class RedisBitmaps { private static final String BASE_KEY = "bloomfilter"; private static final String CURSOR = "cursor"; private JedisUtils jedisUtils; private long bitSize; RedisBitmaps(long bits) { this.jedisUtils = new JedisUtils(); this.bitSize = LongMath.divide(bits, 64, RoundingMode.CEILING) * Long.SIZE;//位數(shù)組的長(zhǎng)度,相當(dāng)于n個(gè)long的長(zhǎng)度 if (bitCount() == 0) { jedisUtils.execute((jedis -> jedis.setbit(currentKey(), bitSize - 1, false))); } } boolean get(long[] offsets) { for (long i = 0; i < cursor() + 1; i++) { final long cursor = i; //只要有一個(gè)cursor對(duì)應(yīng)的bitmap中,offsets全部命中,則表示可能存在 boolean match = Arrays.stream(offsets).boxed() .map(offset -> jedisUtils.execute(jedis -> jedis.getbit(genkey(cursor), offset))) .allMatch(b -> (Boolean) b); if (match) return true; } return false; } boolean get(final long offset) { return jedisUtils.execute(jedis -> jedis.getbit(currentKey(), offset)); } boolean set(long[] offsets) { if (cursor() > 0 && get(offsets)) { return false; } boolean bitsChanged = false; for (long offset : offsets) bitsChanged |= set(offset); return bitsChanged; } boolean set(long offset) { if (!get(offset)) { jedisUtils.execute(jedis -> jedis.setbit(currentKey(), offset, true)); return true; } return false; } long bitCount() { return jedisUtils.execute(jedis -> jedis.bitcount(currentKey())); } long bitSize() { return this.bitSize; } private String currentKey() { return genkey(cursor()); } private String genkey(long cursor) { return BASE_KEY + "-" + cursor; } private Long cursor() { String cursor = jedisUtils.execute(jedis -> jedis.get(CURSOR)); return cursor == null ? 0 : Longs.tryParse(cursor); } void ensureCapacityInternal() { if (bitCount() * 2 > bitSize()) grow(); } void grow() { Long cursor = jedisUtils.execute(jedis -> jedis.incr(CURSOR)); jedisUtils.execute((jedis -> jedis.setbit(genkey(cursor), bitSize - 1, false))); } void reset() { String[] keys = LongStream.range(0, cursor() + 1).boxed().map(this::genkey).toArray(String[]::new); jedisUtils.execute(jedis -> jedis.del(keys)); jedisUtils.execute(jedis -> jedis.set(CURSOR, "0")); jedisUtils.execute(jedis -> jedis.setbit(currentKey(), bitSize - 1, false)); } private PipelineExecutor apply(PipelineExecutor executor) { return executor; } }
下面我們做一個(gè)單元測(cè)試來(lái)驗(yàn)證其正確性。
如果我們插入的數(shù)量等于原預(yù)計(jì)總數(shù),RedisBloomFilter擴(kuò)容了1次,而兩個(gè)布隆過(guò)濾器的結(jié)果一致,都為false,true,false。
如果插入的數(shù)量為原預(yù)計(jì)總數(shù)的3倍,RedisBloomFilter擴(kuò)容了3次,并且仍判斷正確,而Guava布隆過(guò)濾器則在判斷str3時(shí)出現(xiàn)誤判。
public class TestRedisBloomFilter { private static final int TOTAL = 10000; private static final double FPP = 0.0005; @Test public void test() { RedisBloomFilter redisBloomFilter = RedisBloomFilter.create(TOTAL, FPP); redisBloomFilter.resetBitmap(); BloomFilterbloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), TOTAL, FPP); IntStream.range(0, /* 3* */TOTAL).boxed() .map(i -> Hashing.md5().hashInt(i).toString()) .collect(toList()).forEach(s -> { redisBloomFilter.put(s); bloomFilter.put(s); }); String str1 = Hashing.md5().hashInt(99999).toString(); String str2 = Hashing.md5().hashInt(9999).toString(); String str3 = "abcdefghijklmnopqrstuvwxyz123456"; System.out.println(redisBloomFilter.mightContain(str1) + ":" + bloomFilter.mightContain(str1)); System.out.println(redisBloomFilter.mightContain(str2) + ":" + bloomFilter.mightContain(str2)); System.out.println(redisBloomFilter.mightContain(str3) + ":" + bloomFilter.mightContain(str3)); } } >> grow bloomfilter-1 false:false true:true false:false >> grow bloomfilter-1 grow bloomfilter-2 grow bloomfilter-3 false:false true:true false:true
綜上,本文利用了Guava布隆過(guò)濾器的思想,并結(jié)合Redis中的Bitmaps等特性實(shí)現(xiàn)了支持動(dòng)態(tài)擴(kuò)容的布隆過(guò)濾器,它將布隆過(guò)濾器底層的位數(shù)據(jù)裝載到了Redis數(shù)據(jù)庫(kù)中,這樣的好處在于可以部署在更復(fù)雜的多應(yīng)用或分布式系統(tǒng)中,還可以利用Redis完成持久化,定時(shí)過(guò)期等功能。
四、參考文獻(xiàn)吳軍. 數(shù)學(xué)之美[M]. 人民郵電出版社, 2012.
Kirsch A, Mitzenmacher M. Less hashing, same performance: building a better bloom filter[C]//ESA. 2006, 6: 456-467.
Bloom Filters for the Perplexed, https://sagi.io/2017/07/bloom...
Google Guava, https://github.com/google/guava
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/68080.html
摘要:方案二布隆過(guò)濾器攔截布隆過(guò)濾器介紹概念布隆過(guò)濾器英語(yǔ)是年由布隆提出的。這就是布隆過(guò)濾器的基本思想。防緩存穿透的布隆過(guò)濾器判斷是否為合法非法則不允許繼續(xù)查庫(kù)從緩存中獲取數(shù)據(jù)緩存為空從數(shù)據(jù)庫(kù)中獲取緩存空對(duì)象參考書(shū)籍開(kāi)發(fā)與運(yùn)維 上周在工作中遇到了一個(gè)問(wèn)題場(chǎng)景,即查詢商品的配件信息時(shí)(商品:配件為1:N的關(guān)系),如若商品并未配置配件信息,則查數(shù)據(jù)庫(kù)為空,且不會(huì)加入緩存,這就會(huì)導(dǎo)致,下次在查詢同...
摘要:再來(lái)看怎么結(jié)合一起使用根據(jù)給定的布隆過(guò)濾器添加值不能為空根據(jù)給定的布隆過(guò)濾器判斷值是否存在不能為空很簡(jiǎn)單,只有兩個(gè)方法,往里面添加元素,檢查元素是否在里面這里的客戶端使用的是封裝的,可以在我的項(xiàng)目中查看完整的使用代碼。 前言 最近在研究布隆過(guò)濾器(如果不了解什么是布隆過(guò)濾器的,推薦看這篇如何判斷一個(gè)元素在億級(jí)數(shù)據(jù)中是否存在?了解),發(fā)現(xiàn)Guava提供了封裝好的類,但是只能單機(jī)使用,一般...
摘要:可以看出,僅僅從布隆過(guò)濾器本身而言,根本沒(méi)有存放完整的數(shù)據(jù),只是運(yùn)用一系列隨機(jī)映射函數(shù)計(jì)算出位置,然后填充二進(jìn)制向量。也就是說(shuō)布隆過(guò)濾器只能判斷數(shù)據(jù)是否一定不存在,而無(wú)法判斷數(shù)據(jù)是否一定存在。我向布隆過(guò)濾器插入了,然后用來(lái)測(cè)試誤判率。本文是站在小白的角度去討論布隆過(guò)濾器,如果你是科班出身,或者比較聰明,又或者真正想完全搞懂布隆過(guò)濾器的可以移步。 不知道從什么時(shí)候開(kāi)始,本來(lái)默默無(wú)聞的布隆過(guò)濾器...
閱讀 745·2021-10-09 09:44
閱讀 2024·2021-09-22 15:54
閱讀 5064·2021-09-22 10:55
閱讀 1445·2019-08-29 18:41
閱讀 781·2019-08-29 11:24
閱讀 2106·2019-08-28 18:20
閱讀 1032·2019-08-26 11:51
閱讀 3052·2019-08-26 11:00