海量數據處理 算法總結

【Bloom Filter】
Bloom Filter(BF)是一種空間效率很高的隨機數據結構,它利用位數組很簡潔地表示一個集合,並能判斷一個元素是否屬於這個集合。它是一個判斷元素是否存在集合的快速的概率算法。Bloom Filter有可能會出現錯誤判斷,但不會漏掉判斷。也就是Bloom Filter判斷元素不再集合,那肯定不在。如果判斷元素存在集合中,有一定的概率判斷錯誤。因此,Bloom Filter不適合那些“零錯誤”的應用場合。

而在能容忍低錯誤率的應用場合下,Bloom Filter比其他常見的算法(如hash,折半查找)極大節省了空間。 

Bloom Filter的詳細介紹:海量數據處理之Bloom Filter詳解

【適用範圍】 
可以用來實現數據字典,進行數據的判重,或者集合求交集 


【基本原理及要點】 

原理要點:一是位數組, 而是k個獨立hash函數。

1)位數組:

       假設Bloom Filter使用一個m比特的數組來保存信息,初始狀態時,Bloom Filter是一個包含m位的位數組,每一位都置爲0,即BF整個數組的元素都設置爲0。


2)k個獨立hash函數

      爲了表達S={x1, x2,…,xn}這樣一個n個元素的集合,Bloom Filter使用k個相互獨立的哈希函數(Hash Function),它們分別將集合中的每個元素映射到{1,…,m}的範圍中。

          當我們往Bloom Filter中增加任意一個元素x時候,我們使用k個哈希函數得到k個哈希值,然後將數組中對應的比特位設置爲1。即第i個哈希函數映射的位置hashi(x)就會被置爲1(1≤i≤k)。 注意,如果一個位置多次被置爲1,那麼只有第一次會起作用,後面幾次將沒有任何效果。在下圖中,k=3,且有兩個哈希函數選中同一個位置(從左邊數第五位,即第二個“1“處)。   


 3)判斷元素是否存在集合

    在判斷y是否屬於這個集合時,我們只需要對y使用k個哈希函數得到k個哈希值,如果所有hashi(y)的位置都是11ik),即k個位置都被設置爲1了,那麼我們就認爲y是集合中的元素,否則就認爲y不是集合中的元素。下圖中y1就不是集合中的元素(因爲y1有一處指向了“0”位)。y2或者屬於這個集合,或者剛好是一個false positive



      顯然這 個判斷並不保證查找的結果是100%正確的。

Bloom Filter的缺點:

       1)Bloom Filter無法從Bloom Filter集合中刪除一個元素因爲該元素對應的位會牽動到其他的元素。所以一個簡單的改進就是 counting Bloom filter,用一個counter數組代替位數組,就可以支持刪除了。 此外,Bloom Filter的hash函數選擇會影響算法的效果。

       2)還有一個比較重要的問題,如何根據輸入元素個數n,確定位數組m的大小及hash函數個數hash函數選擇會影響算法的效果當hash函數個數k=(ln2)*(m/n)時錯誤率最小。在錯誤率不大於E的情況 下,m至少要等於n*lg(1/E) 才能表示任意n個元素的集合。但m還應該更大些,因爲還要保證bit數組裏至少一半爲0,則m應 該>=nlg(1/E)*lge ,大概就是nlg(1/E)1.44(lg表示以2爲底的對數)。 

舉個例子我們假設錯誤率爲0.01,則此時m應大概是n的13倍。這樣k大概是8個。 

 注意:

         這裏m與n的單位不同,m是bit爲單位,而n則是以元素個數爲單位(準確的說是不同元素的個數)。通常單個元素的長度都是有很多bit的。所以使用bloom filter內存上通常都是節省的。 

       一般BF可以與一些key-value的數據庫一起使用,來加快查詢。由於BF所用的空間非常小,所有BF可以常駐內存。這樣子的話,對於大部分不存在的元素,我們只需要訪問內存中的BF就可以判斷出來了,只有一小部分,我們需要訪問在硬盤上的key-value數據庫。從而大大地提高了效率。

        

【擴展】 
Bloom filter將集合中的元素映射到位數組中,用k(k爲哈希函數個數)個映射位是否全1表示元素在不在這個集合中。Counting bloom filter(CBF)將位數組中的每一位擴展爲一個counter,從而支持了元素的刪除操作。Spectral Bloom Filter(SBF)將其與集合元素的出現次數關聯。SBF採用counter中的最小值來近似表示元素的出現頻率。 


【問題實例】 
給你A,B兩個文件,各存放50億條URL,每條URL佔用64字節,內存限制是4G,讓你找出A,B文件共同的URL。如果是三個乃至n個文件呢? 
根據這個問題我們來計算下內存的佔用,4G=2^32大概是40億*8大概是340億bit,n=50億,如果按出錯率0.01算需要的大概是650億個bit。 現在可用的是340億,相差並不多,這樣可能會使出錯率上升些。另外如果這些urlip是一一對應的,就可以轉換成ip,則大大簡單了。


2. Hash

【什麼是Hash】 
       Hash,一般翻譯做“散列”,也有直接音譯爲“哈希”的,就是把任意長度的輸入(又叫做預映射, pre-image),通過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,而不可能從散列值來唯一的確定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。 
       HASH主要用於信息安全領域中加密算法,它把一些不同長度的信息轉化成雜亂的128位的編碼,這些編碼值叫做HASH值. 也可以說,hash就是找到一種數據內容和數據存放地址之間的映射關係。 
      數組的特點是:尋址容易,插入和刪除困難;而鏈表的特點是:尋址困難,插入和刪除容易。那麼我們能不能綜合兩者的特性,做出一種尋址容易,插入刪除也容易的數據結構?答案是肯定的,這就是我們要提起的哈希表,哈希表有多種不同的實現方法,我接下來解釋的是最常用的一種方法——拉鍊法,(也是樹的一種存儲結構,稱爲二叉鏈表)我們可以理解爲“鏈表的數組”,如圖: 


                                                  
     左邊很明顯是個數組,數組的每個成員包括一個指針,指向一個鏈表的頭,當然這個鏈表可能爲空,也可能元素很多。我們根據元素的一些特徵把元素分配到不同的鏈表中去,也是根據這些特徵,找到正確的鏈表,再從鏈表中找出這個元素。 
元素特徵轉變爲數組下標的方法就是散列法。

散列法當然不止一種,下面列出三種比較常用的:
1,除法散列法 (求模數)
最直觀的一種,上圖使用的就是這種散列法,公式: 
index = value % 16 
學過彙編的都知道,求模數其實是通過一個除法運算得到的,所以叫“除法散列法”。 
2,平方散列法 
求index是非常頻繁的操作,而乘法的運算要比除法來得省時(對現在的CPU來說,估計我們感覺不出來),所以我們考慮把除法換成乘法和一個位移操作。公式: 
index = (value * value) >> 28 
如果數值分配比較均勻的話這種方法能得到不錯的結果,但我上面畫的那個圖的各個元素的值算出來的index都是0——非常失敗。也許你還有個問題,value如果很大,value * value不會溢出嗎?答案是會的,但我們這個乘法不關心溢出,因爲我們根本不是爲了獲取相乘結果,而是爲了獲取index。 
3,斐波那契(Fibonacci)散列法 
平方散列法的缺點是顯而易見的,所以我們能不能找出一個理想的乘數,而不是拿value本身當作乘數呢?答案是肯定的。 
1,對於16位整數而言,這個乘數是40503 
2,對於32位整數而言,這個乘數是2654435769 
3,對於64位整數而言,這個乘數是11400714819323198485 
這幾個“理想乘數”是如何得出來的呢?這跟一個法則有關,叫黃金分割法則,而描述黃金分割法則的最經典表達式無疑就是著名的斐波那契數列,如果你還有興趣,就到網上查找一下“斐波那契數列”等關鍵字,我數學水平有限,不知道怎麼描述清楚爲什麼,另外斐波那契數列的值居然和太陽系八大行星的軌道半徑的比例出奇吻合,很神奇,對麼?
對我們常見的32位整數而言,公式: 
i ndex = (value * 2654435769) >> 28 
如果用這種斐波那契散列法的話,那我上面的圖就變成這樣了: 

                                             
很明顯,用斐波那契散列法調整之後要比原來的取摸散列法好很多。 
【適用範圍】 
快速查找,刪除的基本數據結構,通常需要總數據量可以放入內存。 
【基本原理及要點】 
hash函數選擇,針對字符串,整數,排列,具體相應的hash方法。 
碰撞處理:

一種是open hashing,也稱爲拉鍊法;

另一種就是closed hashing,也稱開地址法,opened addressing。 
【擴展】 
d-left hashing中的d是多個的意思,我們先簡化這個問題,看一看2-left hashing。2-left hashing指的是將一個哈希表分成長度相等的兩半,分別叫做T1和T2,給T1和T2分別配備一個哈希函數,h1和h2。在存儲一個新的key時,同 時用兩個哈希函數進行計算,得出兩個地址h1[key]和h2[key]。這時需要檢查T1中的h1[key]位置和T2中的h2[key]位置,哪一個 位置已經存儲的(有碰撞的)key比較多,然後將新key存儲在負載少的位置。如果兩邊一樣多,比如兩個位置都爲空或者都存儲了一個key,就把新key 存儲在左邊的T1子表中,2-left也由此而來。在查找一個key時,必須進行兩次hash,同時查找兩個位置。 
【問題實例】 
1).海量日誌數據,提取出某日訪問百度次數最多的那個IP。 
IP的數目還是有限的,最多2^32個,所以可以考慮使用hash將ip直接存入內存,然後進行統計。

 

3. Bit-map

【什麼是Bit-map】 
        所謂的Bit-map就是用一個bit位來標記某個元素對應的Value, 而Key即是該元素。由於採用了Bit爲單位來存儲數據,因此在存儲空間方面,可以大大節省。

        如果說了這麼多還沒明白什麼是Bit-map,那麼我們來看一個具體的例子,假設我們要對0-7內的5個元素(4,7,2,5,3)排序(這裏假設這些元素沒有重複)。那麼我們就可以採用Bit-map的方法來達到排序的目的。要表示8個數,我們就只需要8個Bit(1Bytes),首先我們開闢1Byte的空間,將這些空間的所有Bit位都置爲0(如下圖:)
                                                       


然後遍歷這5個元素,首先第一個元素是4,那麼就把4對應的位置爲1(可以這樣操作 p+(i/8)|(0x01<<(i%8)) 當然了這裏的操作涉及到Big-ending和Little-ending的情況,這裏默認爲Big-ending),因爲是從零開始的,所以要把第五位置爲一(如下圖):
 

                                                      


然後再處理第二個元素7,將第八位置爲1,,接着再處理第三個元素,一直到最後處理完所有的元素,將相應的位置爲1,這時候的內存的Bit位的狀態如下: 
 

                                                    


然後我們現在遍歷一遍Bit區域,將該位是一的位的編號輸出(2,3,4,5,7),這樣就達到了排序的目的。下面的代碼給出了一個BitMap的用法:排序。 

C代碼  

  1. //定義每個Byte中有8個Bit位    
  2.      #include <memory.h>    
  3.      #define BYTESIZE 8    
  4.      void SetBit(char *p, int posi)    
  5.      {    
  6.          for(int i=0; i < (posi/BYTESIZE); i++)    
  7.          {    
  8.              p++;    
  9.          }    
  10.           
  11.          *p = *p|(0x01<<(posi%BYTESIZE));//將該Bit位賦值1    
  12.          return;    
  13.     }    
  14.          
  15.     void BitMapSortDemo()    
  16.      {    
  17.          //爲了簡單起見,我們不考慮負數    
  18.         int num[] = {3,5,2,10,6,12,8,14,9};    
  19.           
  20.         //BufferLen這個值是根據待排序的數據中最大值確定的    
  21.         //待排序中的最大值是14,因此只需要2個Bytes(16個Bit)    
  22.         //就可以了。    
  23.         const int BufferLen = 2;    
  24.         char *pBuffer = new char[BufferLen];    
  25.           
  26.         //要將所有的Bit位置爲0,否則結果不可預知。    
  27.         memset(pBuffer,0,BufferLen);    
  28.         for(int i=0;i<9;i++)    
  29.         {    
  30.             //首先將相應Bit位上置爲1    
  31.             SetBit(pBuffer,num[i]);    
  32.         }    
  33.           
  34.         //輸出排序結果    
  35.          for(int i=0;i<BufferLen;i++)//每次處理一個字節(Byte)    
  36.         {    
  37.             for(int j=0;j<BYTESIZE;j++)//處理該字節中的每個Bit位    
  38.             {    
  39.                 //判斷該位上是否是1,進行輸出,這裏的判斷比較笨。    
  40.                 //首先得到該第j位的掩碼(0x01<<j),將內存區中的    
  41.                 //位和此掩碼作與操作。最後判斷掩碼是否和處理後的    
  42.                //結果相同    
  43.                if((*pBuffer&(0x01<<j)) == (0x01<<j))    
  44.                {    
  45.                     printf("%d ",i*BYTESIZE + j);    
  46.                 }    
  47.             }    
  48.             pBuffer++;    
  49.             }    
  50.     }    
  51.          
  52.     int _tmain(int argc, _TCHAR* argv[])    
  53.      {    
  54.          BitMapSortDemo();    
  55.          return 0;    
  56.      }    


【適用範圍】 

可進行數據的快速查找,判重,刪除,一般來說數據範圍是int的10倍以下 

【基本原理及要點】
使用bit數組來表示某些元素是否存在,比如8位電話號碼 

【擴展】 

Bloom filter可以看做是對bit-map的擴展 

【問題實例】 

1)已知某個文件內包含一些電話號碼,每個號碼爲8位數字,統計不同號碼的個數。 

8位最多99 999 999,大概需要99m個bit,大概10幾m字節的內存即可。 (可以理解爲從0-99 999 999的數字,每個數字對應一個Bit位,所以只需要99M個Bit==1.2MBytes,這樣,就用了小小的1.2M左右的內存表示了所有的8位數的電話) 

2)2.5億個整數中找出不重複的整數的個數,內存空間不足以容納這2.5億個整數。 

將bit-map擴展一下,用2bit表示一個數即可,0表示未出現,1表示出現一次,2表示出現2次及以上,在遍歷這些數的時候,如果對應位置的值是0,則將其置爲1;如果是1,將其置爲2;如果是2,則保持不變。或者我們不用2bit來進行表示,我們用兩個bit-map即可模擬實現這個2bit-map,都是一樣的道理。 

 

4. 堆

【什麼是堆】

八大排序裏面有堆 的詳細介紹:八大排序算法
概念:堆是一種特殊的二叉樹,具備以下兩種性質
1)每個節點的值都大於(或者都小於,稱爲最小堆)其子節點的值
2)樹是完全平衡的,並且最後一層的樹葉都在最左邊
這樣就定義了一個最大堆。如下圖用一個數組來表示堆:

                                  

那麼下面介紹二叉堆:二叉堆是一種完全二叉樹,其任意子樹的左右節點(如果有的話)的鍵值一定比根節點大,上圖其實就是一個二叉堆。

你一定發覺了,最小的一個元素就是數組第一個元素,那麼二叉堆這種有序隊列如何入隊呢?看圖:

 


                                                       

假設要在這個二叉堆裏入隊一個單元,鍵值爲2,那隻需在數組末尾加入這個元素,然後儘可能把這個元素往上挪,直到挪不動,經過了這種複雜度爲Ο(logn)的操作,二叉堆還是二叉堆。

那如何出隊呢?也不難,看圖                           

                                      

 

出隊一定是出數組的第一個元素,這麼來第一個元素以前的位置就成了空位,我們需要把這個空位挪至葉子節點,然後把數組最後一個元素插入這個空位,把這個“空位”儘量往上挪。這種操作的複雜度也是Ο(logn)。

【適用範圍】
海量數據前n大,並且n比較小,堆可以放入內存

【基本原理及要點】
最大堆求前n小,最小堆求前n大。方法,比如求前n小,我們比較當前元素與最大堆裏的最大元素,如果它小於最大元素,則應該替換那個最大元 素。這樣最後得到的n個元素就是最小的n個。適合大數據量,求前n小,n的大小比較小的情況,這樣可以掃描一遍即可得到所有的前n元素,效率很高。

【擴展】
雙堆,一個最大堆與一個最小堆結合,可以用來維護中位數。

【問題實例】
1)100w個數中找最大的前100個數。
用一個100個元素大小的最小堆即可。

 

5. 雙層桶

【什麼是雙層桶】  
事實上,與其說雙層桶劃分是一種數據結構,不如說它是一種算法設計思想。面對一堆大量的數據我們無法處理的時候,我們可以將其分成一個個小的單元,然後根據一定的策略來處理這些小單元,從而達到目的。

【適用範圍】 
第k大,中位數,不重複或重複的數字

【基本原理及要點】 
因爲元素範圍很大,不能利用直接尋址表,所以通過多次劃分,逐步確定範圍,然後最後在一個可以接受的範圍內進行。可以通過多次縮小,雙層只是一個例子,分治纔是其根本(只是“只分不治”)。

【擴展】 
當有時候需要用一個小範圍的數據來構造一個大數據,也是可以利用這種思想,相比之下不同的,只是其中的逆過程。

【問題實例】 
1).2.5億個整數中找出不重複的整數的個數,內存空間不足以容納這2.5億個整數。

有 點像鴿巢原理,整數個數爲2^32,也就是,我們可以將這2^32個數,劃分爲2^8個區域(比如用單個文件代表一個區域),然後將數據分離到不同的區 域,然後不同的區域在利用bitmap就可以直接解決了。也就是說只要有足夠的磁盤空間,就可以很方便的解決。 當然這個題也可以用我們前面講過的BitMap方法解決,正所謂條條大道通羅馬~~~

2).5億個int找它們的中位數。

這個例子比上面那個更明顯。首先我們將int劃分爲2^16個區域,然後讀取數據統計落到各個區域裏的數的個數,之後我們根據統計結果就可以判斷中位數落到那個區域,同時知道這個區域中的第幾大數剛好是中位數。然後第二次掃描我們只統計落在這個區域中的那些數就可以了。

實 際上,如果不是int是int64,我們可以經過3次這樣的劃分即可降低到可以接受的程度。即可以先將int64分成2^24個區域,然後確定區域的第幾 大數,在將該區域分成2^20個子區域,然後確定是子區域的第幾大數,然後子區域裏的數的個數只有2^20,就可以直接利用direct addr table進行統計了。

3).現在有一個0-30000的隨機數生成器。請根據這個隨機數生成器,設計一個抽獎範圍是0-350000彩票中獎號碼列表,其中要包含20000箇中獎號碼。

這個題剛好和上面兩個思想相反,一個0到3萬的隨機數生成器要生成一個0到35萬的隨機數。那麼我們完全可以將0-35萬的區間分成35/3=12個區 間,然後每個區間的長度都小於等於3萬,這樣我們就可以用題目給的隨機數生成器來生成了,然後再加上該區間的基數。那麼要每個區間生成多少個隨機數呢?計 算公式就是:區間長度*隨機數密度,在本題目中就是30000*(20000/350000)。最後要注意一點,該題目是有隱含條件的:彩票,這意味着你 生成的隨機數裏面不能有重複,這也是我爲什麼用雙層桶劃分思想的另外一個原因。


6. 數據庫索引及優化

索引是對數據庫表中一列或多列的值進行排序的一種結構,使用索引可快速訪問數據庫表中的特定信息。

數據庫索引

什麼是索引

  數據庫索引好比是一本書前面的目錄,能加快數據庫的查詢速度。
  例如這樣一個查詢:select * from table1 where id=44。如果沒有索引,必須遍歷整個表,直到ID等於44的這一行被找到爲止;有了索引之後(必須是在ID這一列上建立的索引),直接在索引裏面找44(也就是在ID這一列找),就可以得知這一行的位置,也就是找到了這一行。可見,索引是用來定位的。
  索引分爲聚簇索引和非聚簇索引兩種,聚簇索引 是按照數據存放的物理位置爲順序的,而非聚簇索引就不一樣了;聚簇索引能提高多行檢索的速度,而非聚簇索引對於單行的檢索很快。

概述

  建立索引的目的是加快對錶中記錄的查找或排序。
  爲表設置索引要付出代價的:一是增加了數據庫的存儲空間,二是在插入和修改數據時要花費較多的時間(因爲索引也要隨之變動)。

                     

B樹索引-Sql Server索引方式


爲什麼要創建索引

  創建索引可以大大提高系統的性能。
    第一,通過創建唯一性索引,可以保證數據庫表中每一行數據的唯一性。
    第二,可以大大加快數據的檢索速度,這也是創建索引的最主要的原因。
    第三,可以加速表和表之間的連接,特別是在實現數據的參考完整性方面特別有意義。
    第四,在使用分組和排序子句進行數據檢索時,同樣可以顯著減少查詢中分組和排序的時間。
    第五,通過使用索引,可以在查詢的過程中,使用優化隱藏器,提高系統的性能。
  也許會有人要問:增加索引有如此多的優點,爲什麼不對錶中的每一個列創建一個索引呢?因爲,增加索引也有許多不利的方面。
    第一,創建索引和維護索引要耗費時間,這種時間隨着數據量的增加而增加。
    第二,索引需要佔物理空間,除了數據表佔數據空間之外,每一個索引還要佔一定的物理空間,如果要建立聚簇索引,那麼需要的空間就會更大。
    第三,當對錶中的數據進行增加、刪除和修改的時候,索引也要動態的維護,這樣就降低了數據的維護速度。

在哪建索引

  索引是建立在數據庫表中的某些列的上面。在創建索引的時候,應該考慮在哪些列上可以創建索引,在哪些列上不能創建索引。一般來說,應該在這些列上創建索引:
  在經常需要搜索的列上,可以加快搜索的速度;
  在作爲主鍵的列上,強制該列的唯一性和組織表中數據的排列結構;
  在經常用在連接的列上,這些列主要是一些外鍵,可以加快連接的速度;在經常需要根據範圍進行搜索的列上創建索引,因爲索引已經排序,其指定的範圍是連續的;
  在經常需要排序的列上創建索引,因爲索引已經排序,這樣查詢可以利用索引的排序,加快排序查詢時間;
  在經常使用在WHERE子句中的列上面創建索引,加快條件的判斷速度。
  同樣,對於有些列不應該創建索引。一般來說,不應該創建索引的的這些列具有下列特點:
  第一,對於那些在查詢中很少使用或者參考的列不應該創建索引。這是因爲,既然這些列很少使用到,因此有索引或者無索引,並不能提高查詢速度。相反,由於增加了索引,反而降低了系統的維護速度和增大了空間需求。
  第二,對於那些只有很少數據值的列也不應該增加索引。這是因爲,由於這些列的取值很少,例如人事表的性別列,在查詢的結果中,結果集的數據行佔了表中數據行的很大比例,即需要在表中搜索的數據行的比例很大。增加索引,並不能明顯加快檢索速度。
  第三,對於那些定義爲text, image和bit數據類型的列不應該增加索引。這是因爲,這些列的數據量要麼相當大,要麼取值很少,不利於使用索引。
  第四,當修改性能遠遠大於檢索性能時,不應該創建索引。這是因爲,修改性能和檢索性能是互相矛盾的。當增加索引時,會提高檢索性能,但是會降低修改性能。當減少索引時,會提高修改性能,降低檢索性能。因此,當修改操作遠遠多於檢索操作時,不應該創建索引。

數據庫優化

  此外,除了數據庫索引之外,在LAMP結果如此流行的今天,數據庫(尤其是MySQL)性能優化也是海量數據處理的一個熱點。下面就結合自己的經驗,聊一聊MySQL數據庫優化的幾個方面。
  首先,在數據庫設計的時候,要能夠充分的利用索引帶來的性能提升,至於如何建立索引,建立什麼樣的索引,在哪些字段上建立索引,上面已經講的很清楚了,這裏不在贅述。另外就是設計數據庫的原則就是儘可能少的進行數據庫寫操作(插入,更新,刪除等),查詢越簡單越好。如下:

數據庫設計:

. 創建索引

. 查詢語句

1)查詢越簡單越好:單表查詢 > inner join >其他

        2)更新越少越好


  其次,配置緩存是必不可少的,配置緩存可以有效的降低數據庫查詢讀取次數,從而緩解數據庫服務器壓力,達到優化的目的,一定程度上來講,這算是一個“圍魏救趙”的辦法。可配置的緩存包括索引緩存(key_buffer),排序緩存(sort_buffer),查詢緩存(query_buffer),表描述符緩存(table_cache),如下:

配置緩存:

. 索引緩存(key_buffer)

. 排序緩存 (sort_buffer)

. 查詢緩存  (query_buffer)

. 表描述符緩存(table_cache)

 



  第三,切表,切表也是一種比較流行的數據庫優化法。分表包括兩種方式:橫向分表和縱向分表,其中,橫向分表比較有使用意義,故名思議,橫向切表就是指把記錄分到不同的表中,而每條記錄仍舊是完整的(縱向切表後每條記錄是不完整的),例如原始表中有100條記錄,我要切成2個表,那麼最簡單也是最常用的方法就是ID取摸切表法,本例中,就把ID爲1,3,5,7。。。的記錄存在一個表中,ID爲2,4,6,8,。。。的記錄存在另一張表中。雖然橫向切表可以減少查詢強度,但是它也破壞了原始表的完整性,如果該表的統計操作比較多,那麼就不適合橫向切表。橫向切表有個非常典型的用法,就是用戶數據:每個用戶的用戶數據一般都比較龐大,但是每個用戶數據之間的關係不大,因此這裏很適合橫向切表。最後,要記住一句話就是:分表會造成查詢的負擔,因此在數據庫設計之初,要想好是否真的適合切表的優化:

切表分表

. 縱向 :字段較多時可以考慮,一般用處不到

. 橫向 :1)能有效降低表的大小,減少由於枷鎖導致的等待 

             2)查詢會變得複雜,尤其是需要排序的查詢

 

第四,日誌分析,在數據庫運行了較長一段時間以後,會積累大量的LOG日誌,其實這裏面的蘊涵的有用的信息量還是很大的。通過分析日誌,可以找到系統性能的瓶頸,從而進一步尋找優化方案。

數據庫性能分析:

. 查詢吞吐量,數據量監控

. 慢查詢分析:索引,I/O,cpu等。

    

以上講的都是單機MySQL的性能優化的一些經驗,但是隨着信息大爆炸,單機的數據庫服務器已經不能滿足我們的需求,於是,多多節點,分佈式數據庫網絡出現了,其一般的結構如下:

 


                                    



分佈式數據庫結構

這種分佈式集羣的技術關鍵就是“同步複製”。。。

 

 

7. 倒排索引(搜索引擎之基石)

引言:

在信息大爆炸的今天,有了搜索引擎的幫助,使得我們能夠快速,便捷的找到所求。提到搜索引擎,就不得不說VSM模型,說到VSM,就不得不聊倒排索引。可以毫不誇張的講,倒排索引是搜索引擎的基石。

VSM檢索模型

VSM全稱是Vector Space Model(向量空間模型),是IR(Information Retrieval信息檢索)模型中的一種,由於其簡單,直觀,高效,所以被廣泛的應用到搜索引擎的架構中。98年的Google就是憑藉這樣的一個模型,開始了它的瘋狂擴張之路。廢話不多說,讓我們來看看到底VSM是一個什麼東東。

在開始之前,我默認大家對線性代數裏面的向量(Vector)有一定了解的。向量是既有大小又有方向的量,通常用有向線段表示,向量有:加、減、倍數、內積、距離、模、夾角的運算。

文檔(Document):一個完整的信息單元,對應的搜索引擎系統裏,就是指一個個的網頁。

標引項(Term):文檔的基本構成單位,例如在英文中可以看做是一個單詞,在中文中可以看作一個詞語。

查詢(Query):一個用戶的輸入,一般由多個Term構成。

那麼用一句話概況搜索引擎所做的事情就是:對於用戶輸入的Query,找到最相似的Document返回給用戶。而這正是IR模型所解決的問題:

信息檢索模型是指如何對查詢和文檔進行表示,然後對它們進行相似度計算的框架和方法。

舉個簡單的例子:

現在有兩篇文章(Document)分別是 “春風來了,春天的腳步近了” 和 “春風不度玉門關”。然後輸入的Query是“春風”,從直觀上感覺,前者和輸入的查詢更相關一些,因爲它包含有2個春,但這只是我們的直觀感覺,如何量化呢,要知道計算機是門嚴謹的學科^_^。這個時候,我們前面講的Term和VSM模型就派上用場了。

首先我們要確定向量的維數,這時候就需要一個字典庫,字典庫的大小,即是向量的維數。在該例中,字典爲{春風,來了,春天, 的,腳步,近了,不度,玉門關} ,文檔向量,查詢向量如下圖:

 



VSM模型示例

PS:爲了簡單起見,這裏分詞的粒度很大。

將Query和Document都量化爲向量以後,那麼就可以計算用戶的查詢和哪個文檔相似性更大了。簡單的計算結果是D1和D2同Query的內積都是1,囧。當然了,如果分詞粒度再細一些,查詢的結果就是另外一個樣子了,因此分詞的粒度也是會對查詢結果(主要是召回率和準確率)造成影響的。

上述的例子是用一個很簡單的例子來說明VSM模型的,計算文檔相似度的時候也是採用最原始的內積的方法,並且只考慮了詞頻(TF)影響因子,而沒有考慮反詞頻(IDF),而現在比較常用的是cos夾角法,影響因子也非常多,據傳Google的影響因子有100+之多。
大名鼎鼎的Lucene項目就是採用VSM模型構建的,VSM的核心公式如下(由cos夾角法演變,此處省去推導過程)


 

VSM模型公式

從上面的例子不難看出,如果向量的維度(對漢語來將,這個值一般在30w-45w)變大,而且文檔數量(通常都是海量的)變多,那麼計算一次相關性,開銷是非常大的,如何解決這個問題呢?不要忘記了我們這節的主題就是 倒排索引,主角終於粉墨登場了!!!


倒排索引非常類似我們前面提到的Hash結構。以下內容來自維基百科:

倒排索引(英語:Inverted index),也常被稱爲反向索引置入檔案反向檔案,是一種索引方法,被用來存儲在全文搜索下某個單詞在一個文檔或者一組文檔中的存儲位置的映射。它是文檔檢索系統中最常用的數據結構。

有兩種不同的反向索引形式:

  • 一條記錄的水平反向索引(或者反向檔案索引)包含每個引用單詞的文檔的列表。
  • 一個單詞的水平反向索引(或者完全反向索引)又包含每個單詞在一個文檔中的位置。

後者的形式提供了更多的兼容性(比如短語搜索),但是需要更多的時間和空間來創建。

由上面的定義可以知道,一個倒排索引包含一個字典的索引和所有詞的列表。其中字典索引中包含了所有的Term(通俗理解爲文檔中的詞),索引後面跟的列表則保存該詞的信息(出現的文檔號,甚至包含在每個文檔中的位置信息)。下面我們還採用上面的方法舉一個簡單的例子來說明倒排索引。

例如現在我們要對三篇文檔建立索引(實際應用中,文檔的數量是海量的):

文檔1(D1):中國移動互聯網發展迅速

文檔2(D2):移動互聯網未來的潛力巨大

文檔3(D3):中華民族是個勤勞的民族

那麼文檔中的詞典集合爲:{中國,移動,互聯網,發展,迅速,未來,的,潛力,巨大,中華,民族,是,個,勤勞}

建好的索引如下圖:

 



倒排索引

在上面的索引中,存儲了兩個信息,文檔號和出現的次數。建立好索引以後,我們就可以開始查詢了。例如現在有一個Query是”中國移動”。首先分詞得到Term集合{中國,移動},查倒排索引,分別計算query和d1,d2,d3的距離。有沒有發現,倒排表建立好以後,就不需要在檢索整個文檔庫,而是直接從字典集合中找到“中國”和“移動”,然後遍歷後面的列表直接計算。

對倒排索引結構我們已經有了初步的瞭解,但在實際應用中還有些需要解決的問題(主要是由海量數據引起的)。筆者列舉一些問題,並給出相應的解決方案,拋磚以引玉,希望大家可以展開討論:

1.左側的索引表如何建立?怎麼做才能最高效?

可能有人不假思索回答:左側的索引當然要採取hash結構啊,這樣可以快速的定位到字典項。但是這樣問題又來了,hash函數如何選取呢?而且hash是有碰撞的,但是倒排表似乎又是不允許碰撞的存在的。事實上,雖然倒排表和hash異常的相思,但是兩者還是有很大區別的,其實在這裏我們可以採用前面提到的Bitmap的思想,每個Term(單詞)對應一個位置(當然了,這裏不是一個比特位),而且是一一對應的。如何能夠做到呢,一般在文字處理中,有很多的編碼,漢字中的GBK編碼基本上就可以包含所有用到的漢字,每個漢字的GBK編碼是確定的,因此一個Term的”ID”也就確定了,從而可以做到快速定位。注:得到一個漢字的GBK號是非常快的過程,可以理解爲O(1)的時間複雜度。

2.如何快速的添加刪除更新索引?

有經驗的碼農都知道,一般在系統的“做加法”的代價比“做減法”的代價要低很多,在搜索引擎中中也不例外。因此,在倒排表中,遇到要刪除一個文檔,其實不是真正的刪除,而是將其標記刪除。這樣一個減法操作的代價就比較小了。

3.那麼多的海量文檔,如果存儲呢?有麼有什麼備份策略呢?

當然了,一臺機器是存儲不下的,分佈式存儲是採取的。一般的備份保存3份就足夠了。

好了,倒排索引終於完工了,不足的地方請指正。謝謝

8. 外排序

適用範圍:

大數據的排序,去重
 基本原理及要點:

外排序的歸併方法,置換選擇 敗者樹原理,最優歸併樹
 擴展:
 問題實例:
 1).有一個1G大小的一個文件,裏面每一行是一個詞,詞的大小不超過16個字節,內存限制大小是1M。返回頻數最高的100個詞
這個數據具有很明顯的特點,詞的大小爲16個字節,但是內存只有1m做hash有些不夠,所以可以用來排序。內存可以當輸入緩衝區使用。

   

9. trie樹

 適用範圍:

數據量大,重複多,但是數據種類小可以放入內存

基本原理及要點:

實現方式,節點孩子的表示方式
 擴展:

壓縮實現。
 問題實例:
 1).有10個文件,每個文件1G, 每個文件的每一行都存放的是用戶的query,每個文件的query都可能重複。要你按照query的頻度排序 。
 2).1000萬字符串,其中有些是相同的(重複),需要把重複的全部去掉,保留沒有重複的字符串。請問怎麼設計和實現?
 3).尋找熱門查詢:查詢串的重複度比較高,雖然總數是1千萬,但如果除去重複後,不超過3百萬個,每個不超過255字節。  

10. 分佈式處理 mapreduce

基本原理及要點:

將數據交給不同的機器去處理,數據劃分,結果歸約。

擴 展: 
問題實例: 

1).The canonical example application of MapReduce is a process to count the appearances of 

each different word in a set of documents: 
void map(String name, String document): 
// name: document name 
// document: document contents 
for each word w in document: 
EmitIntermediate(w, 1); 

void reduce(String word, Iterator partialCounts): 
// key: a word 
// values: a list of aggregated partial counts 
int result = 0; 
for each v in partialCounts: 
result += ParseInt(v); 
Emit(result); 
Here, each document is split in words, and each word is counted initially with a "1" value by 

the Map function, using the word as the result key. The framework puts together all the pairs 

with the same key and feeds them to the same call to Reduce, thus this function just needs to 

sum all of its input values to find the total appearances of that word. 

2). 海量數據分佈在100臺電腦中,想個辦法高效統計出這批數據的TOP10。 

3).一共有N個機器,每個機器上有N個數。每個機器最多存 O(N)個數並對它們操作。如何找到N^2個數的中數(median)? 


經典問題分析 

上千萬or億數據(有 重複),統計其中出現次數最多的前N個數據,分兩種情況:可一次讀入內存,不可一次讀入。 

可用思路:trie樹+堆,數據庫索引,劃分 子集分別統計,hash,分佈式計算,近似統計,外排序 

所謂的是否能一次讀入內存,實際上應該指去除重複後的數據量。如果去重後數據可 以放入內存,我們可以爲數據建立字典,比如通過 map,hashmap,trie,然後直接進行統計即可。當然在更新每條數據的出現次數的時候,我們可以利用一個堆來維護出現次數最多的前N個數據,當然這樣導致維護次數增加,不如完全統計後在求前N大效率高。 

如果數據無法放入內存。一方面我們可以考慮上面的字典方法能否被改進以適應這種情形,可以做的改變就是將字典存放到硬盤上,而不是內存,這可以參考數據庫的存儲方法。 

當然還有更好的方法,就是可以採用分佈式計算,基本上就是map-reduce過程,首先可以根據數據值或者把數據hash(md5)後的值,將數據按照範圍劃分到不同的機子,最好可以讓數據劃分後可以一次讀入內存,這樣不同的機子負責處理各種的數值範圍,實際上就是map。得到結果後,各個機子只需拿出各自的出現次數最多的前N個數據,然後彙總,選出所有的數據中出現次數最多的前N個數據,這實際上就是reduce過程。 

實際上可能想直接將數據均分到不同的機子上進行處理,這樣是無法得到正確的解的。因爲一個數據可能被均分到不同的機子上,而另一個則可能完全聚集到一個機子上,同時還可能存在具有相同數目的數據。比如我們要找出現次數最多的前100個,我們將1000萬的數據分佈到10臺機器上,找到每臺出現次數最多的前 100個,歸併之後這樣不能保證找到真正的第100個,因爲比如出現次數最多的第100個可能有1萬個,但是它被分到了10臺機子,這樣在每臺上只有1千個,假設這些機子排名在1000個之前的那些都是單獨分佈在一臺機子上的,比如有1001個,這樣本來具有1萬個的這個就會被淘汰,即使我們讓每臺機子選出出現次數最多的1000個再歸併,仍然會出錯,因爲可能存在大量個數爲1001個的發生聚集。因此不能將數據隨便均分到不同機子上,而是要根據hash 後的值將它們映射到不同的機子上處理,讓不同的機器處理一個數值範圍。 

而外排序的方法會消耗大量的IO,效率不會很高。而上面的分佈式方法,也可以用於單機版本,也就是將總的數據根據值的範圍,劃分成多個不同的子文件,然後逐個處理。處理完畢之後再對這些單詞的及其出現頻率進行一個歸併。實際上就可以利用一個外排序的歸併過程。 

另外還可以考慮近似計算,也就是我們可以通過結合自然語言屬性,只將那些真正實際中出現最多的那些詞作爲一個字典,使得這個規模可以放入內存。



本文參考網上資料稍微做了修改。沒有找到是哪個牛人總結的。很想在這標明原著作者,以致尊重他人勞動成果。
同時後續會陸續修改部分內容和詳細介紹每一種方法。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章