人妖在线一区,国产日韩欧美一区二区综合在线,国产啪精品视频网站免费,欧美内射深插日本少妇

新聞動(dòng)態(tài)

淺析python實(shí)現(xiàn)布隆過(guò)濾器及Redis中的緩存穿透原理

發(fā)布日期:2022-01-04 04:22 | 文章來(lái)源:gibhub

在開(kāi)發(fā)軟件時(shí),我們經(jīng)常需要判斷一個(gè)元素是否在一個(gè)集合中,比如,如何判斷單詞的拼寫(xiě)是否錯(cuò)誤(判斷單詞是否在已知的字典中);在網(wǎng)絡(luò)爬蟲(chóng)里,如何確認(rèn)一個(gè)網(wǎng)址是否已經(jīng)爬取過(guò);反垃圾郵件系統(tǒng)中,如何判斷一個(gè)郵件地址是否為垃圾郵件地址等等。

如果這些作為面試題那就很有區(qū)分度了,初級(jí)工程師就會(huì)說(shuō),把全部的元素都存在 hash 表中,當(dāng)需要判斷元素是否在集合中時(shí),可以直接判斷,時(shí)間復(fù)雜度是 O(1),比如 Python 的集合:

>>> all_elements = {'a','b','c','d','e'}
>>> 'a' in all_elements
True
>>> 'f' in all_elements
False
>>>

這樣回答顯然沒(méi)有考慮到數(shù)量級(jí)的問(wèn)題,就拿爬蟲(chóng)中的 URL 去重來(lái)說(shuō),假設(shè)一個(gè) URL 的平均長(zhǎng)度是 64 字節(jié),那單純存儲(chǔ)這 10 億個(gè) URL,需要大約 60GB 的內(nèi)存空間。因?yàn)楣1肀仨毦S持較小的裝載因子,才能保證不會(huì)出現(xiàn)過(guò)多的哈希沖突,導(dǎo)致操作的性能下降。而且,用鏈表法解決沖突的哈希表,還會(huì)存儲(chǔ)鏈表指針,因此哈希表的存儲(chǔ)效率一般只有 50%。所以,如果將這 10 億個(gè) URL 構(gòu)建成哈希表,那需要的內(nèi)存空間會(huì)超過(guò) 100GB。這樣的話一般的機(jī)器,內(nèi)存就不夠用了,更別提哈希查找了。

因此考慮到這一點(diǎn),中級(jí)一點(diǎn)的工程師會(huì)說(shuō)數(shù)據(jù)量大的時(shí)候可以采用分治的思想,用多臺(tái)機(jī)器(比如 20 臺(tái)內(nèi)存是 8GB 的機(jī)器)來(lái)存儲(chǔ)這 10 億網(wǎng)頁(yè)鏈接。這種分治的處理思路,也是一種解決辦法,本質(zhì)上還是增加硬件資源來(lái)解決問(wèn)題,還不夠高級(jí)。

更高級(jí)的工程師會(huì)提到位圖、布隆過(guò)濾器,可以使用很小的內(nèi)存來(lái)實(shí)現(xiàn)大數(shù)據(jù)量的查詢。如果你提到了這兩個(gè)概念,那離 offer 也就不遠(yuǎn)了。想回答好這個(gè)問(wèn)題,看本文就夠了,保證你搞懂布隆過(guò)濾器,要是搞不懂,你加我微信,我會(huì)對(duì)你負(fù)責(zé)的????。

在講布隆過(guò)濾器前,要先講一下另一種存儲(chǔ)結(jié)構(gòu),位圖(BitMap)。因?yàn)椋悸∵^(guò)濾器本身就是基于位圖的,是對(duì)位圖的一種改進(jìn)。

同樣地,為了方便你理解,我們來(lái)簡(jiǎn)化問(wèn)題,現(xiàn)在有 1 千萬(wàn)個(gè)整數(shù),整數(shù)的范圍在 1 到 1 億之間。如何快速查找某個(gè)整數(shù)是否在這 1 千萬(wàn)個(gè)整數(shù)中呢?

當(dāng)然,這個(gè)問(wèn)題還是可以用哈希表來(lái)解決。不過(guò),我們可以使用一種比較“特殊”的哈希表,那就是位圖。我們申請(qǐng)一個(gè)大小為 1 億、數(shù)據(jù)類型為布爾類型(true 或者 false)的數(shù)組。我們將這 1 千萬(wàn)個(gè)整數(shù)作為數(shù)組下標(biāo),將對(duì)應(yīng)的數(shù)組值設(shè)置成 true。比如,整數(shù) 5 對(duì)應(yīng)下標(biāo)為 5 的數(shù)組值設(shè)置為 true,也就是 array[5]=true 。

當(dāng)我們查詢某個(gè)整數(shù) K 是否在這 1 千萬(wàn)個(gè)整數(shù)中的時(shí)候,我們只需要將對(duì)應(yīng)的數(shù)組值 array[K]取出來(lái),看是否等于 true。如果等于 true,那說(shuō)明 1 千萬(wàn)整數(shù)中包含這個(gè)整數(shù) K;相反,就表示不包含這個(gè)整數(shù) K。不過(guò),很多語(yǔ)言中提供的布爾類型,大小是 1 個(gè)字節(jié)的,并不能節(jié)省太多內(nèi)存空間。實(shí)際上,表示 true 和 false 兩個(gè)值,我們只需要用一個(gè)二進(jìn)制位(bit)就可以了。那如何通過(guò)編程語(yǔ)言,來(lái)表示一個(gè)二進(jìn)制位呢?

這個(gè)用語(yǔ)言很難表達(dá),還是給出代碼吧,一碼勝千言啊。

這里先給出 Java 的,我覺(jué)得 Java 的更容易看懂,再給出 Python 的,你可以對(duì)比著代碼看下,應(yīng)該很快就理解了。

public class BitMap { // Java中char類型占16bit,也即是2個(gè)字節(jié)
  private char[] bytes;
  private int nbits;  
  public BitMap(int nbits) {
 this.nbits = nbits;
 this.bytes = new char[nbits/16+1];
  } 
  public void set(int k) {
 if (k > nbits) return;
 int byteIndex = k / 16;
 int bitIndex = k % 16;
 bytes[byteIndex] |= (1 << bitIndex);
  } 
  public boolean get(int k) {
 if (k > nbits) return false;
 int byteIndex = k / 16;
 int bitIndex = k % 16;
 return (bytes[byteIndex] & (1 << bitIndex)) != 0;
  }
}

Python 的實(shí)現(xiàn):

class BitMap(object):
 def __init__(self, max_value):
  """
  使用多個(gè)整型元素來(lái)儲(chǔ)存數(shù)據(jù),每個(gè)元素4個(gè)字節(jié)(32位)
  """
  self._size = int((max_value + 31 - 1) / 31)  # 計(jì)算需要的字節(jié)數(shù),字節(jié)數(shù)也是數(shù)組的大小
  self.array = [0 for i in range(self._size)]  # 數(shù)組的元素都初始化為0,每個(gè)元素有32位
 @staticmethod
 def get_element_index(num):
  """
  獲取該數(shù)即將儲(chǔ)存的字節(jié)在數(shù)組中下標(biāo)
  """
  return num // 31 
 @staticmethod
 def get_bit_index(num):
  """
  獲取該數(shù)在元素中的位下標(biāo)
  """
  return num % 31
 
 def set(self, num):
  """
  將該數(shù)存在對(duì)應(yīng)的元素的對(duì)應(yīng)位置
  """
  element_index = self.get_element_index(num)
  bit_index = self.get_bit_index(num)
  self.array[element_index] = self.array[element_index] | (1 << bit_index)
 def get(self, num):
  """
  查找該數(shù)是否存在與bitmap中
  """
  element_index = self.get_element_index(num)
  bit_index = self.get_bit_index(num)
  if self.array[element_index] & (1 << bit_index):
return True
  return False

從上面位圖實(shí)現(xiàn)的代碼中,你應(yīng)該可以發(fā)現(xiàn),位圖通過(guò)數(shù)組下標(biāo)來(lái)定位數(shù)據(jù),所以,訪問(wèn)效率非常高。而且,每個(gè)數(shù)字用一個(gè)二進(jìn)制位來(lái)表示,在數(shù)字范圍不大的情況下,所需要的內(nèi)存空間非常小。

比如剛剛那個(gè)例子,如果用哈希表存儲(chǔ)這 1 千萬(wàn)的數(shù)據(jù),數(shù)據(jù)是 32 位的整型數(shù),每個(gè)整數(shù) 4 個(gè)字節(jié),那總共至少需要 1 千萬(wàn) * 4 = 40MB 的存儲(chǔ)空間。如果我們通過(guò)位圖的話,數(shù)字范圍在 1 到 1 億之間,只需要 1 億個(gè)二進(jìn)制位,也就是 12MB 左右的存儲(chǔ)空間就夠了。

位圖就是用一個(gè)二進(jìn)制位的 1 來(lái)代表一個(gè)元素存在,是不是挺簡(jiǎn)單的?不過(guò),這里我們有個(gè)假設(shè),就是數(shù)字所在的范圍不是很大。如果數(shù)字的范圍很大,比如剛剛那個(gè)問(wèn)題,數(shù)字范圍不是 1 到 1 億,而是 1 到 10 億,那位圖的大小就需要 10 億個(gè)二進(jìn)制位,也就是 120MB 的大小,消耗的內(nèi)存空間,增加了 10 倍,如何不增加內(nèi)存空間來(lái)解決問(wèn)題呢?請(qǐng)繼續(xù)往下看。

布隆過(guò)濾器的原理

這個(gè)時(shí)候,布隆過(guò)濾器就要出場(chǎng)了。布隆過(guò)濾器就是為了解決剛剛這個(gè)問(wèn)題,對(duì)位圖這種數(shù)據(jù)結(jié)構(gòu)的一種改進(jìn)。

布隆過(guò)濾器是由伯頓·布隆于 1970 年提出的,為了簡(jiǎn)化說(shuō)明布隆過(guò)濾器的原理,我們降低數(shù)據(jù)量級(jí):假如數(shù)字范圍是從 1 到 100 提升到 200,為了不占用太多內(nèi)存,我們依然使用 100 個(gè)二進(jìn)制位,如果數(shù)據(jù)個(gè)數(shù)超過(guò) 100 個(gè),就必然存在哈希沖突,怎么辦?

因?yàn)槲覀兪褂?1 位代表一個(gè)元素,因此 100 個(gè)二進(jìn)制位,最多代表 100 個(gè)元素,但是假如使用 2 位來(lái)代表一個(gè)元素呢?那就是組合 C(100,2) = 100*99/2 = 4950 個(gè)數(shù),是不是可以代表更多?當(dāng)然了,你還可以使用 3 位代表一個(gè)元素,這樣可以代表 161700 個(gè)數(shù)。

我們以使用 2 位二進(jìn)制位來(lái)代表一個(gè)元素為例,設(shè)計(jì)兩個(gè) HASH 函數(shù),bit1 = HASH1(num),bit2 = HASH2(num),存入 num 時(shí)就把 bit1 和 bit2 都置為 1;判斷時(shí)就判斷 bit1 和 bit2,當(dāng) bit1 和 bit2 都為 1 時(shí),就表示 num 存在集合中。

這樣會(huì)有個(gè)問(wèn)題:兩個(gè)數(shù) num1 和 num2 經(jīng)過(guò)兩個(gè) HASH 函數(shù)之后,結(jié)果一樣,也就是存在 HASH 沖突,這樣就可能誤判。

實(shí)際上,只要讓誤判的概率足夠低,結(jié)果就是可信的。假設(shè)哈希函數(shù)的個(gè)數(shù)為 k,二進(jìn)制的位數(shù)為 m,元素個(gè)數(shù) n,可以從數(shù)學(xué)上計(jì)算出他們與誤判率的關(guān)系[1](原麥迪遜威斯康星大學(xué)曹培教授提供):

可以看出,當(dāng) m/n = 16,n = 8,時(shí),誤判率為萬(wàn)分之五,這在大多數(shù)應(yīng)用中都是可以接受的,而且這種誤判是這樣的:如果這個(gè)元素在集合中,那么布隆過(guò)濾器絕不會(huì)漏掉,如果不在集合中,則有可能判定為在集合中,比如說(shuō)對(duì)應(yīng)垃圾郵件,布隆過(guò)濾器絕不會(huì)漏掉黑名單中的任何一個(gè)可疑地址,但是它有一定極小的概率將一個(gè)不在黑名單上的電子郵件判定為在黑名單中。

到這里我相信你已經(jīng)明白了個(gè)中原理。

在 Python 中使用布隆過(guò)濾器

pypi 搜了了 Python 中的布隆過(guò)濾器,有 3 個(gè):

pip install bloom-filter2
pip install pybloom-live
pip install bloompy

第三個(gè) bloompy[2] 的文檔比較詳細(xì),推薦使用(如果有興趣,你可以自己實(shí)現(xiàn)一個(gè)):

bloompy 提供了四種布隆過(guò)濾器:

1、標(biāo)準(zhǔn)布隆過(guò)濾器。

標(biāo)準(zhǔn)布隆過(guò)濾器只能進(jìn)行數(shù)據(jù)的查詢和插入,是其他過(guò)濾器的基類,可以進(jìn)行過(guò)濾器的存儲(chǔ)和恢復(fù),代碼示例:

>>> import bloompy
>>> bf = bloompy.BloomFilter(error_rate=0.001,element_num=10**3)
# 查詢?cè)厥欠裨谶^(guò)濾器里返回狀態(tài)標(biāo)識(shí)
# 如果不在里面則插入,返回False表示元素不在過(guò)濾器里
>>> bf.add(1) 
False
>>> bf.add(1)
True
>>> 1 in bf
True
>>> bf.exists(1)
True
>>> bf.add([1,2,3])
False
>>> bf.add([1,2,3])
True
>>> [1,2,3] in bf
True
>>> bf.exists([1,2,3])
True
# 將過(guò)濾器存儲(chǔ)在一個(gè)文件里
>>> bf.tofile('filename.suffix') 
# 從一個(gè)文件里恢復(fù)過(guò)濾器。自動(dòng)識(shí)別過(guò)濾器的種類。
>>> recovered_bf = bloompy.get_filter_fromfile('filename.suffix') 
# 或者使用過(guò)濾器類的類方法 'fromfile' 來(lái)進(jìn)行過(guò)濾器的復(fù)原。對(duì)應(yīng)的類只能恢復(fù)對(duì)應(yīng)的過(guò)濾器
>>> recovered_bf = bloompy.BloomFilter.fromfile('filename.suffix') 
# 返回已經(jīng)插入的元素個(gè)數(shù)
>>> bf.count
2 
# 過(guò)濾器的容量
>>> bf.capacity
1000
# 過(guò)濾器的位向量
>>> bf.bit_array
bitarray('00....')
# 過(guò)濾器位數(shù)組長(zhǎng)度
>>> bf.bit_num
14400
# 過(guò)濾器的哈希種子,默認(rèn)是素?cái)?shù),可修改
>>> bf.seeds
[2, 3, 5, 7, 11,...]
# 過(guò)濾器哈希函數(shù)個(gè)數(shù)
>>> bf.hash_num
10
 

2、計(jì)數(shù)布隆過(guò)濾器。

它是標(biāo)準(zhǔn)布隆過(guò)濾器的子類,但是可以執(zhí)行刪除操作。內(nèi)置默認(rèn)使用 4 位二進(jìn)制位來(lái)表示標(biāo)準(zhǔn)布隆過(guò)濾器的 1 個(gè)位,從而實(shí)現(xiàn)可以增減。

>>> import  bloompy
>>> cbf  = bloompy.CountingBloomFilter(error_rate=0.001,element_num=10**3)
# 與標(biāo)準(zhǔn)布隆過(guò)濾器一樣
>>> cbf.add(12)
False
>>> cbf.add(12)
True
>>> 12 in cbf
True
>>> cbf.count
1 
# 查詢?cè)貭顟B(tài)返回標(biāo)識(shí),如果元素存在過(guò)濾器里則刪除
>>> cbf.delete(12)
True
>>> cbf.delete(12)
False
>>> 12 in cbf
False
>>> cbf.count
0
# 從文件中恢復(fù)過(guò)濾器
>>> recovered_cbf = bloompy.CountingBloomFilter.fromfile('filename.suffix')
 

3、標(biāo)準(zhǔn)擴(kuò)容布隆過(guò)濾器。

當(dāng)插入的元素個(gè)數(shù)超過(guò)當(dāng)前過(guò)濾器的容量時(shí),自動(dòng)增加過(guò)濾器的容量,默認(rèn)內(nèi)置一次擴(kuò)容 2 倍。支持查詢和插入功能。

>>> import bloompy
>>> sbf = bloompy.ScalableBloomFilter(error_rate=0.001,initial_capacity=10**3)
# 默認(rèn)初次可以設(shè)置容量1000
>>> len(sbf)
0
>>> 12 in sbf
False
>>> sbf.add(12)
False
>>> 12 in sbf 
True
>>> len(sbf)
1
>>> sbf.filters
[<bloompy.BloomFilter object at 0x000000000B6F5860>]
>>> sbf.capacity
1000
#當(dāng)過(guò)濾器的元素個(gè)數(shù)達(dá)到容量極限時(shí),過(guò)濾器會(huì)自動(dòng)增加內(nèi)置的標(biāo)準(zhǔn)過(guò)濾器,
#每次增加2倍容量,自動(dòng)實(shí)現(xiàn)擴(kuò)容
>>> for i in range(1000):
  sbf.add(i)
>>> 600 in sbf
True
>>> len(sbf)
2
>>> sbf.filters
[<bloompy.BloomFilter object at 0x000000000B6F5860>, <bloompy.BloomFilter object at 0x000000000B32F748>]
>>> sbf.capacity
3000 
# 從文件中恢復(fù)過(guò)濾器
>>> recovered_sbf = bloompy.ScalableBloomFilter.fromfile('filename.suffix')
 

4、計(jì)數(shù)擴(kuò)容布隆過(guò)濾器。

它是標(biāo)準(zhǔn)擴(kuò)容布隆過(guò)濾器的子類,但支持刪除元素的操作。

>>> import bloompy
>>> scbf = bloompy.SCBloomFilter(error_rate=0.001,initial_capacity=10**3)
>>> scbf.add(1)
False
>>> 1 in scbf
True
>>> scbf.delete(1)
True
>>> 1 in scbf
False
>>> len(scbf)
1
>>> scbf.filters
[<bloompy.CountingBloomFilter object at 0x000000000B6F5828>]
# 插入元素使其達(dá)到過(guò)濾器當(dāng)前容量極限值
>>> for i in range(1100):
  scbf.add(i)
>>> len(scbf)
2
>>> scbf.filters
[<bloompy.CountingBloomFilter object at 0x000000000B6F5828>, <bloompy.CountingBloomFilter object at 0x000000000B6F5898>]
# 從文件中恢復(fù)過(guò)濾器
>>> recovered_scbf = bloompy.SCBloomFilter.fromfile('filename.suffix')
 

Redis 中使用布隆過(guò)濾器

你可以手動(dòng)為 Redis 安裝 RedisBloom 插件,也可以直接使用官方[3]提供的 docker 版本:

docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest

然后在 Redis 中就可以這樣使用:

127.0.0.1:6379> BF.ADD newFilter foo
(integer) 1
127.0.0.1:6379> BF.EXISTS newFilter foo
(integer) 1
127.0.0.1:6379> BF.EXISTS newFilter notpresent
(integer) 0
127.0.0.1:6379> BF.MADD myFilter foo bar baz
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> BF.MEXISTS myFilter foo nonexist bar
1) (integer) 1
2) (integer) 0
3) (integer) 1
 

其實(shí) Redis 中使用布隆過(guò)濾器還有一個(gè)很大的用處,就是處理緩存穿透。Redis 大部分情況都是通過(guò) Key 查詢對(duì)應(yīng)的值,假如發(fā)送的請(qǐng)求傳進(jìn)來(lái)的 key 是不存在 Redis 中的,那么就查不到緩存,查不到緩存就會(huì)去數(shù)據(jù)庫(kù)查詢。假如有大量這樣的請(qǐng)求,這些請(qǐng)求像“穿透”了緩存一樣直接打在數(shù)據(jù)庫(kù)上,這種現(xiàn)象就叫做緩存穿透。

解決方案:

1、把無(wú)效的 Key 存進(jìn) Redis中。如果 Redis 查不到數(shù)據(jù),數(shù)據(jù)庫(kù)也查不到,我們把這個(gè) Key 值保存進(jìn)Redis,設(shè)置 value="null",當(dāng)下次再通過(guò)這個(gè) Key 查詢時(shí)就不需要再查詢數(shù)據(jù)庫(kù)。這種處理方式肯定是有問(wèn)題的,假如傳進(jìn)來(lái)的這個(gè)不存在的 Key 值每次都是隨機(jī)的,那存進(jìn) Redis 也沒(méi)有意義。

2、使用布隆過(guò)濾器。布隆過(guò)濾器的作用是某個(gè) key 不存在,那么就一定不存在,它說(shuō)某個(gè) key 存在,那么很大可能是存在(存在一定的誤判率)。于是我們可以在緩存之前再加一層布隆過(guò)濾器,在查詢的時(shí)候先去布隆過(guò)濾器查詢 key 是否存在,如果不存在就直接返回。

最后的話

布隆過(guò)濾器的數(shù)學(xué)原理在于兩個(gè)完全隨機(jī)的數(shù)字相沖突的概率很小,因此可以在很小的誤判率條件下用很少的空間存儲(chǔ)大量的信息,解決誤判的常見(jiàn)辦法,就是在建立一個(gè)小的白名單,存儲(chǔ)那些可能被誤判的信息。

參考資料

[1]

關(guān)系:

http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html

[2]

bloompy:

https://github.com/01ly/bloompy/blob/master/zh-cn.md

[3]

官方:

https://oss.redis.com/redisbloom/Quick_Start/

版權(quán)聲明:本站文章來(lái)源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請(qǐng)保持原文完整并注明來(lái)源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來(lái)源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來(lái),僅供學(xué)習(xí)參考,不代表本站立場(chǎng),如有內(nèi)容涉嫌侵權(quán),請(qǐng)聯(lián)系alex-e#qq.com處理。

相關(guān)文章

實(shí)時(shí)開(kāi)通

自選配置、實(shí)時(shí)開(kāi)通

免備案

全球線路精選!

全天候客戶服務(wù)

7x24全年不間斷在線

專屬顧問(wèn)服務(wù)

1對(duì)1客戶咨詢顧問(wèn)

在線
客服

在線客服:7*24小時(shí)在線

客服
熱線

400-630-3752
7*24小時(shí)客服服務(wù)熱線

關(guān)注
微信

關(guān)注官方微信
頂部