下圖是簡單動態字符串(simple dynamic string, SDS)的結構表示redis
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字符串,都須要對數組作一次內存重分配:
因爲內存重分配涉及複雜的算法,比較耗時。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。
每一個鏈表節點使用一個adlist.h/listNode結構來表示:
多個listNode能夠經過prev和next指針組成雙端鏈表:
list結構表示:
Redis鏈表特性:
3.1 字典結構
Redis字典使用哈希表做爲底層實現,一個哈希表裏面能夠有多個哈希表節點,而每一個哈希表節點就保存了字典中的一個鍵值對。
哈希表結構及哈希表節點定義:
數據存放以下所示:
Redis中字典由dict.h/dict結構表示:
type屬性和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
哈希表的擴展和收縮
知足下面任一條件,哈希表會進行擴展操做:
負載因子的計算:哈希表已保存節點數量/哈希表大小
當負載因子小於0.1時,程序自動對哈希表進行收縮操做。
Redis對字典的哈希表執行rehash的步驟以下:
1) 爲字典ht[1]哈希表分配空間:
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操做
若是一個有序集合包含的元素數量比較多,或者有序集合中元素的成員是比較長的字符串時,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)用於記錄兩個節點之間的距離:
遍歷操做使用前進指針便可完成,跨度是用來計算排位的(節點在跳躍表中的位置)。
4. 後退指針
節點的後退指針(backward屬性)用於從表尾向表頭訪問節點。
5. 分值和成員
節點的分值(score屬性)是一個double類型的浮點數,跳躍表中的全部節點都按分值從小到大來排序。
節點的成員對象(obj屬性,惟一的)是一個指針,它指向一個字符串對象,而字符串對象則保存一個SDS值。
4.3 跳躍表
zskiplist結構的定義以下:
header和tail指針分別指向跳躍表的表頭和表尾節點,經過這兩個指針,程序定位表頭節點和表尾節點的複雜度爲O(1).
length屬性記錄節點的數量,程序能夠在O(1)複雜度內返回跳躍表的長度。
level屬性用於得到跳躍表中層高最大的節點的層數量,表頭節點的層高不計算在內。
4.4 跳躍表經常使用API
5.1 整數集合的實現
其中encoding有三種取值:
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.1 壓縮列表的構成
壓縮列表是Redis爲了節約內存而開發的,由一系列特殊編碼地連續內存塊組成的順序型數據結構。一個壓縮列表能夠包含任意多個節點(entry),每一個節點能夠保存一個字節數組或者一個整數值。
下圖爲壓縮列表各組成部分:
圖7-2展現了一個壓縮列表實例:
6.2 壓縮列表節點的構成
下圖爲壓縮列表各個組成部分:
1. previous_entry_length
節點的previous_entry_length屬性以字節爲單位,記錄了壓縮列表中前一個節點的長度:
若是有一個指向當前節點起始地址的指針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) 。
可是,儘管連鎖更新的複雜度較高,可是真正形成性能問題的概率很低:
壓縮列表API