第一章 數據結構

  1. 簡單動態字符串

  下圖是簡單動態字符串(simple dynamic string, SDS)的結構表示redis

   

  • free屬性的值爲0,表示這個SDS沒有分配任何未使用空間
  • len屬性的值爲5,表示這個SDS保存了一個5字節長的字符串
  • buf屬性是一個char類型的數組,數組的前五個字節分別保存了R, e, d, i, s五個字符,最後一個字節則保存了空字符'\o'

  SDS遵循C語言字符串以空字符結尾的慣例,保存空字符的1字節空間不計算在SDS的len屬性裏面,這個空字符對SDS的使用者來講是徹底透明的。遵循空字符結尾的好處是,能夠重用一些C字符串函數庫中的函數。例如,有一個指向圖2-1所示SDS的指針s,那麼能夠直接使用<stdio.h>/printf函數,經過執行如下語句:printf("%s", s->buf);算法

  1.1 SDS與C字符串的區別數組

  1.1.1 常數複雜度得到字符串長度安全

  由於C字符串並不記錄自身長度信息,因此爲了獲取一個C字符串長度,程序必須遍歷整個字符串,對遇到的每一個字符進行計數,直到遇到表明字符串結尾的空字符爲止,這個操做的複雜度爲O(N),以下圖所示。服務器

  

  經過訪問SDS的len屬性,Redis將獲取字符串長度所需的複雜度從O(N)下降到了O(1)。 數據結構

  1.1.2 杜絕緩衝區溢出app

  <string.h>/strcat函數能夠將src字符串中的內容拼接到dest字符串的末尾: char *strcat (char *dest, const char *src)函數

  strcat函數假定用戶在執行這個函數時,已經爲dest分配了足夠多的內存,若是假設不成立,則會形成緩衝區溢出,致使s2保存的內容被意外修改了。性能

  

        

  當SDS API須要對SDS進行修改時,API先檢查SDS的空間是否知足修改所需的要求,若是不知足,API會自動將SDS的空間擴展至執行修改所需的大小,而後再進行修改。用戶不須要手動修改SDS空間的大小。 優化

           

  1.1.3 減小修改字符串時帶來的內存重分配次數

  對於一個包含N個字符的C字符串,底層實現是一個N+1個字符長的數組。每次縮短或者增長C字符串,都須要對數組作一次內存重分配:

  • 若是程序執行的是增加字符串的操做,好比拼接(append),那麼執行這個操做以前,程序須要先經過內存重分配來擴展底層數組的空間大小——否則會形成緩衝區溢出
  • 對於縮短字符串操做,好比截斷(trim)操做,須要釋放再也不須要的內存空間,否則會形成內存泄漏

  因爲內存重分配涉及複雜的算法,比較耗時。SDS經過空間預分配和惰性空間釋放兩種優化策略減小內存重分配次數

  1. 空間預分配

  空間預分配用於優化SDS的字符串增加操做:當SDS的API對一個SDS進行修改,並須要對SDS進行空間擴展,程序不只會分配必須的空間,還會爲SDS分配額外的未使用空間。若是SDS長度小於1MB,程序分配和len屬性一樣大小的未使用空間;若是長度大於1MB,程序分配1MB的未使用空間。

  

   

  2. 惰性空間釋放

  當SDS的API須要縮短SDS保存的字符串時,程序並不當即使用內存重分配來回收縮短後多出來的字節,而是使用free屬性將這些字節的數量記錄起來,以備未來使用。

  1.1.4 二進制安全

  C字符串中的字符必須符合某種編碼(好比ASCII),而且除了字符串的末尾以外,字符串裏面不能包含空字符,不然最早被程序讀入的空字符將被誤認爲是字符串結尾。這些限制使得C字符串只能保存文本數據,而不能保存圖片、音頻、視頻、壓縮文件這樣的二進制數據。

  SDS API都會以處理二進制的方式來處理SDS存放在buf數組裏的數據,程序不會對其中的數據作任何限制、過濾或者假設,數據寫入是什麼樣子,讀取時就是什麼樣子。

  1.1.5 兼容部分C字符串函數

  經過在buf數組分配空間時多分配一個字節來容納空字符,使得SDS能夠重用一部分<string.h>庫定義的函數。表2-2列出了SDS主要操做API。

        

 

           

          

  2. 鏈表

  每一個鏈表節點使用一個adlist.h/listNode結構來表示:

   

   多個listNode能夠經過prev和next指針組成雙端鏈表:

  

  list結構表示:

   

  Redis鏈表特性:

  • 雙端:鏈表節點帶有prev和next指針,獲取某個節點的前置節點和後置節點的複雜度都是O(1)
  • 無環:表頭節點的prev指針和表尾節點的next指針都指向NULL,對鏈表的訪問以NULL爲終點
  • 帶表頭指針和表尾指針:經過list結構的head指針和tail指針,程序獲取鏈表的表頭節點和表尾節點的複雜度爲O(1)
  • 帶鏈表長度計數器:程序獲取鏈表中節點數量的複雜度爲O(1)
  • 多態:節點使用void*指針保存節點值,因此鏈表能夠保存各類不一樣類型的值

   

     

  3. 字典

  3.1 字典結構

  Redis字典使用哈希表做爲底層實現,一個哈希表裏面能夠有多個哈希表節點,而每一個哈希表節點就保存了字典中的一個鍵值對。

  哈希表結構及哈希表節點定義: 

                                   

  數據存放以下所示:

   

  Redis中字典由dict.h/dict結構表示:

  

  type屬性和privdata屬性是針對不一樣類型的鍵值對,爲建立多態字典而設置的:

  • type屬性是一個指向dictType結構的指針,每一個dictType結構保存了一簇用於操做特定類型鍵值對的函數,Redis會爲用途不一樣的字典設置不一樣的類型特定函數
  • privdata屬性則保存了須要傳給那些類型特定函數的可選參數

  

  ht[1]哈希表會在對ht[0]哈希表進行rehash時使用。

   

  3.2 哈希算法

  Redis計算哈希值和索引值的方法以下:

  hash = dict->type->hashFunction(key);

  index = hash & dict->ht[x].sizemask;

   index表示放在dictEntry中的哪一個槽內

  3.3 解決鍵衝突

  當兩個或以上的鍵被分配到了哈希表數組的同一個索引上面時,稱爲發生了哈希碰撞。Redis的哈希表經過使用鏈地址法來解決鍵衝突,被分配到同一個索引上的多個節點能夠用這個單向鏈表鏈接起來。

  由於dictEntry節點組成的鏈表沒有指向鏈表表尾的指針,因此爲了速度考慮,程序老是將節點添加到鏈表的表頭位置(複雜度爲O(1)),排在其餘全部節點前面。

   

  3.4 rehash

  哈希表的擴展和收縮

  知足下面任一條件,哈希表會進行擴展操做:

  • 服務器沒有執行BGSAVE或BGREWRITEAOF命令,而且哈希表的負載因子大於等於1
  • 服務器正在執行BGSAVE或BGREWRITEAOF命令,而且哈希表的負載因子大於等於5

  負載因子的計算:哈希表已保存節點數量/哈希表大小

  當負載因子小於0.1時,程序自動對哈希表進行收縮操做。

  Redis對字典的哈希表執行rehash的步驟以下:

  1) 爲字典ht[1]哈希表分配空間:

  • 若是執行的是擴展操做,那麼ht[1]的大小爲第一個大於等於ht[0].used*2n
  • 若是執行的是收縮操做,那麼ht[1]的大小也爲第一個大於等於ht[0].used*2n

  2) 將保存在ht[0]中的全部鍵值對rehash到ht[1]上面

  3) 當ht[0]包含的全部鍵值對都遷移到ht[1]後,釋放ht[0],將ht[1]設置爲ht[0],並在ht[1]新建立一個空白哈希表,爲下一次rehash作準備

  示例步驟以下所示:

      

      

  3.5 漸進式rehash

  擴展或收縮哈希表須要將ht[0]裏面的全部鍵值對rehash到ht[1]裏面,可是,這個rehash動做並非一次性、集中式地完成的,而是屢次、漸進式地完成的。具體步驟以下:

  1)爲ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個哈希表

  2)在字典中維持一個索引計數器變量rehashidx,並將它的值設爲0,表示rehash工做正式開始

  3) 在rehash期間,每次對字典執行添加、刪除、查找或者更新操做時,程序除了執行指定的操做之外,還會順帶將ht[0]哈希表在rehashidx索引上的全部鍵值對rehash到ht[1],當rehash完成後,將rehashidx屬性值加一

  4) 隨着字典操做不斷執行,最後在某個時間點上,ht[0]的全部鍵值對都會被rehash到ht[1],這時將rehashidx設置爲-1,表示rehash操做結束。

               

                          

  字典主要API操做

       

  4. 跳躍表

   若是一個有序集合包含的元素數量比較多,或者有序集合中元素的成員是比較長的字符串時,Redis就會使用跳躍表來做爲有序集合鍵的底層實現。例如,fruit-price是一個有序集合鍵,以水果名爲成員,水果價錢爲分值。fruit-price有序集合的全部數據都保存在一個跳躍表裏面,其中每一個跳躍表節點會保存一款水果的信息,全部水果按價錢由低到高排序。

  4.1 跳躍表的實現

  Redis跳躍表由redis.h/zskiplistNode(用於表示跳躍表節點)和redis.h/zskiplist(保存跳躍表節點相關信息)兩個結構定義,示例以下:

           

  4.2 跳躍表節點

  跳躍表節點的實現由redis.h/zskiplistNode結構定義:

           

 

 

   1. 層

  每次建立一個新的跳躍表節點時,程序都根據冪次定律(power low,越大的數出現的機率越小)隨機生成一個介於1和32之間的值做爲level數組的大小,即層的高度,如圖5-2所示。

  跳躍表節點中每一個元素包含一個指向其餘節點的指針,程序能夠經過這些層加快訪問其餘節點的速度,通常來講,層的數量越多,訪問其餘節點的速度就越快。

  2. 前進指針

  每一個層都有一個指向表尾方向的前進指針(level[i].forward屬性),用於從表頭向表尾方向訪問節點。圖5-3用虛線表示了程序從表頭向表尾方向,遍歷跳躍表中全部節點的路徑。

  3. 跨度

  層的跨度(level[i].span)用於記錄兩個節點之間的距離:

  • 兩個節點之間的跨度越大,相距的越遠
  • 指向NULL的全部前進指針的跨度都爲0

  遍歷操做使用前進指針便可完成,跨度是用來計算排位的(節點在跳躍表中的位置)。

  4. 後退指針

  節點的後退指針(backward屬性)用於從表尾向表頭訪問節點。

  5. 分值和成員

  節點的分值(score屬性)是一個double類型的浮點數,跳躍表中的全部節點都按分值從小到大來排序。

  節點的成員對象(obj屬性,惟一的)是一個指針,它指向一個字符串對象,而字符串對象則保存一個SDS值。

   4.3 跳躍表

  zskiplist結構的定義以下:

  

     

 

  header和tail指針分別指向跳躍表的表頭和表尾節點,經過這兩個指針,程序定位表頭節點和表尾節點的複雜度爲O(1).

  length屬性記錄節點的數量,程序能夠在O(1)複雜度內返回跳躍表的長度。

  level屬性用於得到跳躍表中層高最大的節點的層數量,表頭節點的層高不計算在內。   

  4.4 跳躍表經常使用API

   

  5. 整數集合

  5.1 整數集合的實現

  其中encoding有三種取值:

  • INTSET_ENC_INT16   每一個項是int16_t類型的整數值(取值範圍:-215~215-1)
  • INTSET_ENC_INT32   每一個項是int32_t類型的整數值(取值範圍:-231~231-1) 
  • INTSET_ENC_INT64   每一個項是int64_t類型的整數值(取值範圍:-263~263-1)

  

  5.2 升級

  若是新添加的元素類型比整數集合現有全部元素的類型都要長時,整數集合須要先進行升級(upgrade),而後再將新元素添加到整數集合裏面。升級分三步進行:

  1) 根據新元素的類型,擴展整數集合底層數組的空間大小,併爲新元素分配空間

  2) 將底層數組現有的全部元素都轉換成與新元素相同的類型,並將類型轉換後的元素放置到正確的位置上,並且在放置元素過程當中,須要維持底層數組的有序性質不變

  3) 將元素添加到底層數組裏面

  contents數組中原先包含3個,每一個佔用16位空間的元素。以後須要插入一個佔用32位空間的元素65535,其過程以下所示:

             

     

   5.3 升級的好處

  5.3.1 提高靈活性

  整數集合能夠經過自動升級底層數組來適應新元素,能夠隨意地將int16_t,int32_t或int64_t類型的整數添加到集合中,沒必要擔憂出現類型錯誤。

  5.3.2 節約內存

  儘可能先用int16_t類型來保存元素,在必要時進行升級,以節約內存。

  5.4 降級

  整數集合不支持降級。

  5.5 整數集合API

   

  6. 壓縮列表

  6.1 壓縮列表的構成

  壓縮列表是Redis爲了節約內存而開發的,由一系列特殊編碼地連續內存塊組成的順序型數據結構。一個壓縮列表能夠包含任意多個節點(entry),每一個節點能夠保存一個字節數組或者一個整數值。

  下圖爲壓縮列表各組成部分:

  

  

  圖7-2展現了一個壓縮列表實例:

  • zlbytes屬性地值位0x50(十進制80),表示壓縮列表總長80字節
  • zltail屬性值爲0x3c(十進制60),這表示若是咱們有一個指向壓縮列表起始地址地指針p,那麼p+60,就能夠計算出表尾節點entry3的地址
  • zllen屬性值爲0x3,表示壓縮列表包含三個節點

   

  6.2 壓縮列表節點的構成

  下圖爲壓縮列表各個組成部分:

  

  1. previous_entry_length

  節點的previous_entry_length屬性以字節爲單位,記錄了壓縮列表中前一個節點的長度:

  • 若是前一個節點長度小於254字節,previous_entry_length屬性的長度爲1字節
  • 若是前一個字節長度大於等於254字節,previous_entry_length的長度爲5字節,第一個字節會被設置爲0xFE(十進制254),以後的四個字節用於保存前一節點的長度 

   

  若是有一個指向當前節點起始地址的指針c,能夠根據下圖計算出前一個節點的指針p。壓縮列表從表尾向表頭遍歷操做就是基於這一原理實現的。

   

  2. encoding

  節點encoding屬性記錄了節點的content屬性所保存數據的類型及長度。

  值的最高位爲00、0一、10是字節數組編碼,數組的長度由編碼除去最高兩位以後的其餘位記錄

  

  值的最高位以11開頭的是整數編碼

   

  3. content  

    

  6.3 連鎖更新

  下面討論一種對列表插入或刪除節點時,會發生的極端狀況——連鎖更新。

  前面小節提到過,若是前一個節點長度小於254字節,那麼previous_entry_length屬性須要用1字節來保存長度值;若是前一個節點長度大於254字節,那麼previous_entry_length屬性須要用5字節來保存長度值。

  如今,考慮這樣一種狀況,在一個壓縮列表中,存在多個連續的、長度介於250字節到253字節之間的節點e1至eN,以下圖所示:

  

  若是將一個長度大於等於254字節的新節點new設置爲壓縮列表的表頭節點,以下圖所示:

  

 

  此時e1節點的previous_entry_length屬性從原來的1字節擴展爲5字節,e1本來的長度介於250字節到253字節之間,因爲previous_entry_length長度改變,e1的長度變爲介於254字節到257字節,從而引起e2的更新,e2又會引起e3的擴展,直到eN爲止,並將此過程稱爲連鎖更新。下圖是另外一種引發連鎖更新的狀況:

  

  由於連鎖更新在最壞的狀況下須要對壓縮列表執行N次空間重分配操做,而每次空間重分配的最壞複雜度是O(N),因此連鎖更新的最壞複雜度爲O(N2) 。

  可是,儘管連鎖更新的複雜度較高,可是真正形成性能問題的概率很低:

  • 壓縮列表裏面須要剛好有多個連續的、長度介於250字節到253字節之間的節點
  • 即便出現連鎖更新,只要被更新的節點很少,就不會對性能形成太大影響

  壓縮列表API  

  

相關文章
相關標籤/搜索