圖解redis之數據結構篇
一.簡單動態字元串(sds)
動態,即靈活性,可變化,主要是針對final String 和char[]而言,我們先看下它的數據結構
struct sdshdr{ char [] buf; //位元組數組,用來保存字元串 int len;// buf中已使用的長度,不包括 ,等於sds所保存的字元串長度 int free;// 記錄buf數組剩餘空間的長度}
假設現在又sds為buf分配5個字元,5個空餘空間,則可用下圖表示
那麼,我們談談這樣設計的好處:
- 常數時間獲取字元串長度
由於它底層保存了len,因此可以直接獲取sds字元串長度,所以它是以O(1)的時間複雜度獲取字元串長度;
2. 避免緩衝區溢出
它與char[]數組長度固定不同的是:它每次在插入元素前會先檢查sds空間是否滿足,若不滿足,則做適當的擴充.因此避免了緩衝區溢出的可能性;
此外,在C字元串中,字元串形式固定為"sdds ",即保存了n個字元的字元串是通過char[n+1]實現的,因此c字元串的長度和底層char數組是固定的,因為每次插入或刪除字元,都要重新分配新的內存,來保存新值,因此它是一個很耗時的操作,那麼sds是怎麼優化的呢?
3. 減少了連續內存重分配的次數
sds採用空間預分配和惰性空間釋放兩種策略減少了內存重分配的次數
- 空間預分配:用於優化SDS的字元串增長操作,當sds的api對sds添加內容時,程序不僅會為sds分配修改後所需的空間,還會分配額外的空間(free),具體策略如下
- 如果現有空間足夠存放修改後內容,則不進行擴展,直接插入
- 如果現有空間不足以存放修改後內容,且在修改後sds的len小於1MB,則擴展後的空間len==free,比如修改後空間為15byte,那麼分配的空間為31byte:15byte(len)+15byte(free)+1byte
- 如果不足以存放修改後內容,且修改後len>1MB,則分配1MB的未使用時間.比如修改後為20MB,則新空間為20MB(len)+1MB(free)+1byte.
- 惰性空間釋放:用於優化SDS縮減操作.當api對sds進行縮短操作後,程序不會釋放縮短後空餘出的空間,而是使用free屬性把他們記錄下來,當我們插入的時候就可以減少內存重分配了.
當然,你也不需要擔心,我們以後不添加內容時,造成的內存泄露,因為sds提供了釋放空間的API
4. 二進位安全
C字元串的字元必須某種編碼(比如ASCII),並且除末尾外,其餘地方不能出現空字元串 ,否則會被提前結束,( 是c字元串的結束標識符),比如acvf sdsdsd ,只能識別到acvf.
而這個特性註定它只能保存文本數據,而不能保存圖片,音頻等二進位數據.為了確保redis適用於不同的應用場景,SDS的API都是二進位安全的,即所有的SDS都會以處理二進位方式來處理SDS中buf數組內容,程序不會對它的內容進行限制和過濾,以保證它的原有狀態.這也是把buf設計成位元組數組的原因.舉個例子,由於sds是根據len屬性來判斷字元串的結束,所以對下圖數據我們將讀取到redis cluster
5. 兼容部分C字元串函數
由於SDS的buf數組以 結尾,符合c字元串特徵,因此,它可以使用C字元串的一些api
二.鏈表
struct listNode{ struct listNode *prev//前置節點 struct listNode *next//後置節點 void *value //節點值}listNode;struct list{ listNode *head; listNode *tail; unsigned long len//常數時間獲取鏈表長度 ....}list;
鏈表結構為雙向無環結構(tail.next==null,header.pre==null),可直接獲取節點個數,它被廣泛用在redis的各種功能,如列表鍵,發布和訂閱,慢查詢,監視器等
三.字典(dict)
字典是一種用來存放鍵值對的數據結構,它在redis中應用非常廣泛,增刪改查都是基於它,如set a hello 就是創建了一個key為a值為hello的鍵值對.
它使用哈希表作為底層實現,一個哈希表中可有多個哈希節點,每個節點又保存一個鍵值對
哈希表節點:
struct ditch{ dictEntry **table //哈希表數組 unsigned long len//table.size unsigned long sizemask//哈希表大小的掩碼,等於size-1,用來計算索引值 unsigned long used //該哈希表已有節點的數量}ditch;struct dictEntry{ void *key; union{void val; uint64_tu64; int64_ts64}v; struct dictEntry *next//指向下個節點形成鏈表,以解決哈希衝突}
字典:dict(摘自redis設計與實現)
對於hash演算法以及hash求索引和java中的HashMap思路大同小異,這裡就不提了,下面說說它的重新散列rehash
- rehash
隨著操作的不斷執行,哈希表鍵值對數目也會變化,為了始終控制負載因子在合適的範圍,程序需要對哈希表大小進行擴展或收縮,擴展和收縮工作可通過重新散列完成,步驟如下:
- 為字典的h[1]哈希表分配空間,大小取決於要執行的操作:擴展操作則h[1].size為第一個大於h[0].size*2的2^n,若是收縮,則h[1].size為第一個大於h[0].size的2^n,如h[0].size=3,那麼擴展時3*2=6,則h[1].size=2^3,若收縮,則為2^2
- 把h[0]中所有鍵值對重新散列(rehash)到h[1]
- 當2完成後,釋放h[0],把h[1]設置為h[0],並在h[1]新建一個空白hash表,為下次rehash做準備
四. 跳躍表
跳躍表是一種有序的數據結構,通過在節點中維持多個指向其它節點的指針,達到快速訪問的目的.由於節點中按照score排序,所以被用來實現sorted set,下面貼出它的示意圖(null標誌著遍歷結束)
下面我們先介紹它的跳躍節點:一個跳躍節點由以下幾部分組成:- 層level:跳躍表節點的level數組可以包含多個元素,每個元素都是一個指向其他節點的指針.層由前進指針和跨度組成,這兩個屬性可以唯一確定一個前方節點.
- 後退節點backword(bw),指向前置節點的指針
- 分值(score):score,double類型,跳躍表的節點是按分值大小升序排列,如果分值相同,按照成員對象在字典序中的大小排序
- 成員對象obj,指向一個字元串對象
下面看看跳躍表節點的數據結構代碼
typedef struct zskiplistNode { robj *obj;//成員對象obj double score;//分值 struct zskiplistNode *backward;//後退節點 struct zskiplistLevel { struct zskiplistNode *forward;//前進節點 unsigned int span;//跨度 } level[];//層數組,元素指向其他節點(表頭除外) } zskiplistNode
再看看跳躍表的數據結構代碼
typedef struct zskiplist { struct zskiplistNode *header, *tail;//頭尾 unsigned long length;//長度,以O(1)獲取跳躍表長度 int level;//表中層最大的節點的層數 } zskiplist;
五. 整數集合
整數集合(intset)是集合建的底層實現之一,當一個集合只包含整數值元素,且數量不多時,redis會採用intset作為集合建的底層實現.它可以保存int16_,int32_,int64_的整數值,並且保證集合中元素不重複,接下來看看它的數據結構代碼typedef struct intset { uint32_t encoding; //編碼方式 uint32_t length; //集合中包含的元素數量 int8_t contents[]; //保存元素的數組} intset;
需要提一點:contents數組存放的元素類型是由encoding決定,encodin是int16_,int32_,int64_,contents元素類型也將對應為int16_,int32_,int64_
- 升級
每當我們要將一個新元素添加到整數集合裡面,並且新元素的類型比整數集合現在所有元素的類型都要長時,整數集合需要先進行升級(upgrade),然後才能將新元素添加到整數集合.
1. 根據新元素的類型,擴展整數集合底層數組的空間大小,並為新元素分配空間。
2. 將底層數組現有的所有元素都轉換成與新元素相同的類型,並將新類型轉換後的元素放置到正確的位上,而且在放置元素的過程中,需要繼續維持底層數組的有序性質不變。
3.將新元素添加到底層數組裡面。
- 升級的好處:
- 提升整數集合的靈活性:由於可以自動升級,我們可以隨意的插入元素
- 儘可能地節約內存:C字元串中要讓一個數組同時保存16,32,64位是直接聲明一個int64數組,而redis的intset它可以先用16位存,等遇到更大的時,再升級,避免了盲目使用大內存
整數集合不支持降級操作,一旦對數組進行了升級,編碼就會一直保持升級後的狀態。
最後看看intset數據結構示意圖:
redis的基本數據結構已經介紹完,接下來讓我們學習 圖解redis之對象
推薦閱讀:
※Redis源碼剖析--跳躍表zskiplist
※redis使用消息隊列的場合?
※使用telnet連接redis
※Redis內部數據結構詳解(5)——quicklist
TAG:Redis |