學習之Redis(二)

Redis的對象和數據結構

1、字符串對象(請參考學習之Redis(一):http://www.javashuo.com/article/p-mljrtexn-bz.html

2、列表對象

  列表對象的編碼能夠是ziplist(壓縮列表)或者linkedlist(雙端鏈表),當列表對象包含的元素比較少時會會使用壓縮列表不然會使用雙端鏈表。具體策略是,當列表對象同時知足如下兩個條件時,將使用壓縮列表編碼:html

  一、列表對象保存的全部字符串元素的長度都小於64個字節
node

  二、列表對象保存的元素數量小於512個redis

若是上述兩個條件的任何一個不能被知足,將使用雙端鏈表編碼,以上兩個條件的上限值是能夠經過配置文件中的list-max-ziplist-valuelist-max-ziplist-entries來修改。數據庫

若是壓縮列表編碼的列表對象,再也不知足上述兩個條件時,將會被轉換爲雙端列表編碼的格式,這種策略的優勢是:api

  一、由於壓縮列表比雙端鏈表更節省內存,而且在元素數量較少時,在內存中以連續塊方式保存的壓縮列表比起雙端鏈表能夠更快的被載入到緩存中。
  二、隨着列表對象包含的元素愈來愈多,使用壓縮列表來保存元素的優點逐漸消失,對象就會將底層實現從壓縮列表轉向功能更強、也更適合保存大量元素的雙端鏈表上面。
數組

【壓縮列表--ziplist】

  【結構】緩存

    壓縮列表(ziplist)是列表對象哈希對象的底層數據結構,壓縮列表的節點能夠保存一個字節數組或者一個整數值。服務器

    對於列表對象而言,當一個列表鍵只包含少許列表項,而且存儲的整數、字符串都是比較短小的,將使用壓縮列表。數據結構

    對於哈希對象而言,當一個哈希鍵只包含少許鍵值對,而且鍵和值是小的整數或短的字符串時,將使用壓縮列表。函數

    壓縮列表的總體數據結構以下圖所示:

    

 

 

     壓縮列表編碼的列表對象結構圖以下所示:

    

 

 

     壓縮列表的各個屬性含義:

      zlbytes屬性:記錄壓縮列表佔用的總的內存字節數;在對壓縮列表進行內存重分配或計算壓縮列表末端位置(即zlend屬性所在位置)時須要用到該屬性。

      zltail屬性:記錄壓縮列表的最後一個列表節點的位置距離整個列表起始地址有多少字節,經過該屬性,能夠直接定位表尾節點的地址。

      zllen屬性:記錄壓縮列表的節點數量,當節點數量小於UNINT16_MAX(65535)時,該屬性會記錄節點數量的值,當節點數量大於65535時就再也不存儲,須要遍歷整個壓縮列表才能知道節點的總數量。

      entryX屬性:列表節點,用於存儲實際的數值,每一個壓縮列表節點都由previous_entry_length、encoding、content三個部分組成。

        previous_entry_length屬性:記錄了前一個節點的長度。

        encoding屬性:實際要保存值的數據類型及長度。

        content屬性:實際在節點中保存的值,能夠是一個字節數組或一個整數。

      zlend屬性:是一個特殊值OxFF(十進制255),用於標記壓縮列表的末端。

    包含三個節點的壓縮列表結構示意圖:

      

    【列表節點中3個屬性的詳細說明】

      一、previous_entry_length屬性:
        該屬性記錄了壓縮列表中前一個節點的長度,單位是字節,長度能夠是1字節或5字節,具體策略爲:
        若是前一個節點的長度小於254字節previous_entry_length屬性將用1個字節的長度
        若是前一個節點的長度大於等於254字節,previous_entry_length屬性將用5個字節的長度,第1個字節爲固定的OxFE(十進制254),以後的4個字節被用來保存前一個節點的長度。

        壓縮列表從表位向表頭遍歷的原理:
        向壓縮列表中存儲數據的時候,最早存儲的數據被放在列表尾部,新插入的數據會被放到舊數據的前面,讀取壓縮列表的順序是由表尾到表頭(讀取最舊的數據到最新的數據)。
        在讀取數據的時候,會先讀取zltail屬性,經過該屬性能夠直接定位到壓縮列表的表尾節點,而後經過previous_entry_length屬性,依次找到上一個節點的數據,由後向前,能夠依次讀取出全部的數據。

      二、encoding屬性
        該屬性記錄了實際要保存值的數據類型及長度。

      三、content屬性
        該屬性記錄了保存節點的值,節點值能夠是一個字節數組如」hello world」或者整數。

  總結:壓縮列表是一種爲節約內存而開發的順序型數據結構。
     壓縮列表能夠包含多個節點,每一個節點能夠保存一個字節數組或整數值,能夠被用於列表對象和哈希對象。
     添加新節點到壓縮列表,或者從壓縮列表中刪除節點,可能會引起連鎖更新操做,但這種概率並不高。

【雙端鏈表--linkedlist】

  當一個列表鍵包含了數量比較多的元素,或者列表中包含的元素都是比較長的字符串時,redis會使用鏈表做爲列表鍵的底層實現。除了list列表對象、zset有序集合對象用到了鏈表以外,發佈與訂閱、慢查詢、監視器等功能也用到了鏈表,redis服務器自己還使用鏈表來保存多個客戶端的狀態信息,以及使用鏈表來構建客戶端輸出緩衝區(output buffer)。

  【鏈表結構】

    鏈表結構由list+listnode兩部分組成。

    list結構:
      head:表頭節點。
      tail:表尾節點。
      len:鏈表所包含的節點數量。
      dup:節點值複製函數,用於複製鏈表節點所保存的值。
      free:節點值釋放函數,用戶釋放鏈表節點所保存的值。
      match:節點值對比函數,用於對比鏈表節點所保存的值和另外一個輸入值是否相等。

    dup、free、match是用於實現多態鏈表所需的類型特定函數。

    鏈表節點(listNode)結構:
      prev 前置節點。
      next 後置節點。
      value 節點的值。

    

  【鏈表結構的特色】

    雙端:鏈表節點帶有prevnext指針,獲取某個節點的前置節點和後置節點的複雜度都是O(1)。
    無環:表頭節點的prev指針和表尾節點的next指針都指向NULL,對鏈表的訪問以NULL爲終點。
    有表頭指針和表尾指針:經過list結構的head指針和tail指針,程序獲取鏈表的表頭節點和表尾節點的複雜度爲O(1)。
    有鏈表長度計數器:list結構的len屬性能夠對鏈表節點進行計數,程序獲取鏈表節點數量的複雜度爲O(1)。
    多態:鏈表節點使用value屬性保存節點值,而且能夠經過list結構的dup、free、match三個屬性爲節點值設置類型特定函數,因此鏈表能夠用於保存各類不一樣類型的值。

  linkedlist編碼的列表對象,在存儲上有如下特色:

    inkedlist編碼的列表對象,每一個雙端鏈表節點(node)都保存了一個字符串對象中的字符串,即字符串對象嵌套在列表結構中,以下圖所示:

      

3、哈希對象

  哈希對象的編碼能夠是ziplist(壓縮列表)或者Hashtable(字典),當哈希對象同時知足如下兩個條件時,哈希對象使用壓縮列表編碼:

  一、哈希對象保存的全部鍵值對的鍵和值的字符串的長度都小於64個字節

  二、哈希對象保存的鍵值對數量小於512個

當其中一個條件不能知足時,哈希對象使用hashtable(字典)編碼的格式,這兩個條件的上限值能夠經過配置文件中的hash-max-ziplist-value選項和hash-max-ziplist-entries選項來修改。

若是壓縮列表編碼的哈希對象,再也不知足上述兩個條件時,將被轉換爲字典編碼的格式。

壓縮列表編碼的哈希對象在存儲上有如下特色:

  一、保存了同一鍵值對的兩個節點老是緊挨在一塊兒,保存鍵的節點在前,保存值的節點在後

  二、先添加到哈希對象的鍵值對會被放在壓縮列表的表頭方向,而後來添加到哈希對象中的鍵值對會被放在壓縮列表的表尾方向。

  結構示意圖以下:

 

 【字典】

  特色:字典是以key-value方式存儲數據的一種結構,字典中的每一個鍵都是獨一無二的,程序能夠在字典中根據鍵查找、更新、刪除與之關聯的值,當字典中存儲的數據過多或過少,都會經過rehash從新分配字典的大小和結構

  用途:一、redis的數據庫就是使用字典來做爲底層實現的,對數據庫的增刪改查是構建在對字典的操做之上的;

     二、是哈希鍵的底層實現之一,當一個哈希鍵包含的鍵值對比較多,或者鍵值對中的元素都是比較長的字符串時,redis會使用字典做爲哈希鍵的底層實現;

     三、是集合底層的實現之一。

  結構:字典+哈希表+哈希節點。如圖:

   

   其中dict是字典結構,dictht是哈希表的結構,dictEntry*是哈希節點的結構,最右邊是哈希節點保存的鍵值對的值,k0、k1是鍵,v0、v1是對應的值。

 【字典結構】

  字典結構dict.h/dict有如下幾個參數:

  

  具體說明以下:
    ht屬性是一個包含兩個項的數組,數組中的每個項都是一個dictht哈希表,通常狀況下,字典只是用ht[0]哈希表,ht[1]哈希表只會在rehash時使用。
    rehashidx屬性,記錄了rehash的進度,若是沒有在rehash,則值爲-1。
    typedicttype屬性,保證了redis能夠存儲不一樣類型的鍵值對

  【哈希表結構】一個空的哈希表(沒有任何鍵值對)總體結構圖以下所示:

  

   哈希表由dict.h/dictht結構定義,裏面具體有table、size、used、sizemask幾個屬性:

  

   具體說明以下:
    table屬性是一個數組,數組中的每一個元素都是一個指向dict.h/dictEntry(哈希表節點)結構的指針,每一個dictEntry結構保存着一個鍵值對。
    size屬性記錄了哈希表的大小,也即table數組的大小。
    used屬性記錄了哈希表目前已有節點(鍵值對)的數量。
    sizemask屬性的值老是等於size-1,這個屬性和哈希值一塊兒決定一個鍵應該被放到table數組的哪一個索引上面。

  【哈希表節點】

  

   由於dictentry結構中並無屬性指向鏈表表尾的指針,因此爲了速度考慮,程序老是將新節點添加到鏈表的表頭位置(複雜度O(1)),排在其它已有節點的前面。

  

   【rehash--從新散列

  爲保持哈希表中數據分配的合理性,當哈希表保存的鍵值對數量太多或者太少時,程序須要對哈希表的大小進行相應的擴展和收縮,對哈希表的擴展和收縮是經過執行rehash(從新散列)操做完成的,redis對字典的哈希表進行rehash的步驟以下:

    一、爲字典的ht[1]哈希表分配空間,分配策略爲:
      若是執行的是擴展操做,那麼ht[1]的大小爲第一個大於等於ht[0].used2的2的n次冪(示例:若是used=3,size=3,32=6,第一個大於等於6且是2的n次冪的值是8,因此ht[1]的size大小爲8)。
      若是執行的是收縮操做,那麼ht[1]的大小爲第一個大於等於ht[0].used的2的n次冪(示例:若是used=3,但size=10,ht[1]的size值將爲4,used是哈希表中實際存儲的數據的節點數,size是哈希表大小便可以存儲多少個數據節點)。
    二、將保存在ht[0]中的全部鍵值對rehash到ht[1]。
    三、釋放ht[0],將ht[1]設置爲ht[0],並在ht[1]新建一個空白哈希表,爲下一次rehash作準備。

  【哈希表的擴展與收縮】

  當哈希表中的數據過於緊密或疏鬆時,會對哈希表進行擴展與收縮,具體策略以下:
    負載因子 = 哈希表已保存節點數量 / 哈希表大小
    load_factor = ht[0].used/ht[0].size
  當哈希表的負載因子小於0.1時,程序自動開始對哈希表執行收縮操做。
  當如下條件中的任意一個被知足時,程序會自動開始對哈希表執行擴展操做:
    1.服務器目前沒有在執行BGSAVEBGREWRITEAOF命令,而且哈希表的負載因子大於等於1
    2.服務器目前正在BGSAVEBGREWRITEAOF命令,而且哈希表的負載因子大於等於5
  當正在執行BGSAVE或BGREWRITEAOF命令時,redis會建立子進程,會用到寫時複製技術,須要佔用部份內存,此時儘量避免對哈希表進行擴展,避免消耗沒必要要的內存。

  【漸進式rehash】

  爲了不rehash對服務器性能形成影響,服務器不是一次將ht[0]裏面全部的鍵值對所有rehash到ht[1],而是分批次漸進式的rehash,在漸進式rehash期間,字典會同時使用ht[0]、ht[1]兩個哈希表,期間對字典的查、刪、改都是在兩個哈希表中進行的。若是要在字典裏查找一個鍵,會先在ht[0]裏面查找,若是沒找到,會繼續到ht[1]裏進行查找。可是新增長的鍵值對一概會被保存到ht[1]中,因此ht[0]裏的鍵值對只減不增。

  字典編碼的哈希對象,在存儲上有如下特色:字典中的每一個鍵、值都是一個字符串對象

  

4、集合對象

  集合對象能夠用來存儲不重複的數據。集合對象的編碼能夠是intset(整數集合)或者hashtable(字典):

  

  具體策略爲:
    當集合對象同時知足如下兩個條件時,將使用intset編碼:
      一、集合對象保存的全部元素都是整數值
      二、集合對象保存的元素數量不超過512個
不能知足這兩個條件的集合對象須要使用hashtable編碼,上述兩個條件能夠經過配置文件中的set-max-intset-entries選項來修改。
當intset編碼的集合對象再也不知足上述兩個條件時,將會轉換爲hashtable編碼的格式。

【整數集合】

  整數集合(intset)是用來存儲整數集合的結構,它能夠保存int16_t、int32_t或int64_t的整數,而且保證數據不會重複。

  【結構】

  

  contents數組裏記錄了具體的數據,數組中的數據按值的大小從小到大有序地排列。
  length屬性記錄了數據的個數,即contents數組的長度。
  enconding屬性記錄了數組中數據的類型,能夠爲intset_enc_int1六、intset_enc_int3二、intset_enc_int64,對應的類型分別是int16_t、int32_t、int64_t 。

  

  【升級】

    當新添加的數據比整數集合現有的數據類型都要長時,須要先對整數集合進行升級,而後再添加新數據。
    升級步驟:
      根據新數據的類型,擴展整數集合底層數組的空間大小。
      將原有數據的類型均轉換爲新類型。
      添加新數據。

  由於每次向整數集合添加新元素均可能引發升級,而每次升級都須要對底層數組中已有的全部元素進行類型轉換,因此向整數集合添加新元素的時間複雜度爲O(N)。
  整數集合不支持降級操做,一旦對數組進行了升級,編碼就會一直保持升級後對狀態。

5、有序集合對象

  有序集合對編碼能夠是ziplist或者skiplist
    具體策略規則是:
    當有序集合對象能夠同時知足如下兩個條件時,將使用ziplist編碼:
      一、有序集合保存的元素數量小於128個
         二、有序集合保存的全部元素成員對長度都小於64字節
不能知足以上兩個條件的有序集合對象都使用skiplist編碼,以上兩個條件的上限值能夠經過配置文件中的zset-max-ziplist-entrieszset-max-ziplist-value選項來修改。
當ziplist編碼的有序集合再也不符合上述兩個條件時,將被轉換爲skiplist編碼的格式。

  ziplist編碼的有序集合的存儲方式:

  ziplist編碼以壓縮列表結構做爲底層實現,每一個集合元素使用兩個緊挨在一塊兒對壓縮列表節點來保存。第一個節點保存元素的成員(member),第二個元素保存元素的分值(score)。壓縮列表內對元素按分值大小進行拍下,分值較小對元素放在靠近表頭對位置,結構示意圖以下所示:

  

  壓縮列表結構以前已進行過詳細說明,這裏就再也不贅述,下面將詳細說明下skiplist編碼。

【Skiplist-字典+跳躍表】

  skiplist編碼的有序集合對象使用zset結構做爲底層實現,一個zset結構同時包含一個字典和一個跳躍表,dict指針指向的是字典結構,zsl指針指向的是跳躍表結構,見下圖。

  

 

   字典結構以前已有過詳細說明,這裏再也不贅述,下面先說一下跳躍表結構:

  【跳躍表】

  若是一個有序集合包含的元素數量比較多或者保存的字符串長度較長redis將使用跳躍表做爲有序集合鍵的底層實現。redis只在兩個地方用到了跳躍表,一個是實現有序集合鍵,另外一個是在集羣節點中用做內部數據結構。

  【跳躍表結構】
  跳躍表由zskiplistzskiplistNode兩個結構組成,其中zskiplist用於保存跳躍表信息(好比表頭節點、表尾節點、長度),而zskiplistNode則用於表示跳躍表節點

  

   上圖左側是zskiplist結構,其包含如下屬性:
    Header:指向跳躍表的表頭節點
    Tail:指向跳躍表的表尾節點
    Level:記錄目前跳躍表內,層數最大的那個節點的層數(表頭節點的層數不計算在內)。
    Length:記錄跳躍表的長度,即目前節點的數量。

  上圖右側是四個zskiplistNode結構,該結構有如下屬性:
    層(level):L一、L二、L3等字樣表示各節點的層,連線上帶有數字箭頭的是前進指針,箭頭上的數字表示跨度。前進節點能夠挨個遍歷,也能夠一次跳過多個節點
    後退(backward)指針:節點中用BW字樣標記的是後退指針。後退節點每次只能退至前一個節點。
    分值(score):各個節點中的1.0、2.0和3.0是節點保存的分值,在跳躍表中,節點按各自所保存的分值從小到大排列
    成員對象(obj):各個節點中的o一、o2和o3是節點所保存的成員對象。

 【分值和成員】
    節點的分值(score屬性)是一個double類型的浮點數,跳躍表中的全部節點都按分值從小到大來排序。
    節點的成員對象(obj屬性)是一個指針,它指向一個字符串對象,而字符串對象則保存着一個SDS值。
    在同一個跳躍表中,各個節點保存的成員對象必須是惟一的,可是多個節點保存的分值卻能夠是相同的,分值相同的節點將按照成員對象在字典序中的大小來進行排序,成員對象較小的節點會排在前面(靠近表頭的位置)。

  

 

   zset結構中的zsl跳躍表按分值從小到大保存了全部集合元素,每一個跳躍表節點都保存了一個集合元素:跳躍表節點的object屬性保存了元素的成員,而跳躍表節點的score屬性則保存了元素的分值。經過這個跳躍表,程序能夠對有序集合進行範圍操做,如zrank、zrange等命令就是基於跳躍表api來實現的。
  除此以外,zset結構中的dict字典爲有序集合建立了一個從成員到分值的映射,字典中的每一個鍵值對都保存了一個集合元素:字典的鍵保存了元素的成員,而字典的值則保存了元素的分值。經過這個字典,程序能夠用O(1)複雜度查找給定成員的分值,zscore命令就是根據這一特性實現的,而不少其它有序集合命令都在實現的內部用到了這一特性。
  有序集合每一個元素的成員都是一個字符串對象,而每一個元素的分值都是一個double類型的浮點數。值得一提的是,雖然zset結構同時使用跳躍表和字典來保存有序集合元素,但這兩種數據結構都會經過指針來共享相同元素的成員和分值,因此同時使用跳躍表和字典來保存集合元素不會產生任何重複成員或分值,也不會所以浪費額外的內存。

 


參考資料:雲棲社區

相關文章
相關標籤/搜索