Redis設計與實現讀書筆記-數據結構與對象

一.簡單動態字符串

簡單動態字符串(simple dynamic string,SDS)是redis的默認字符串表示,除此之外,SDS還被用做緩衝區(AOF模塊中的AOF緩衝區和客戶端狀態中的輸入緩衝區),AOF模塊緩衝區指的是在做AOF備份的時候新增加的指令會緩衝到緩衝區,之後再發起部分同步到磁盤;客戶端狀態的輸入緩衝區是指在服務端保存着客戶端輸入指令的一個緩衝區.

SDS的表示結構如下:

struct sdshdr{
    //記錄buf數組中已使用的字節的數量,等於SDS所保存字符串的長度
    int len;
    //保存buf數組中未使用字節的數量
    int free;
    //字節數組,用於保存字符串
    char buf[]
}

以上兩圖爲SDS存儲示例.圖1中free爲0,表示該SDS沒有分配任何未使用空間,len=5表示存儲的字符串字節長度,buf指向保存數據的數組,結尾保存空字符串'\0',這是爲了遵循C的保存習慣,以便可以使用部分C的函數,但不計入len的統計中.圖二中free=5表示除了保存的redis字符串之外,還分配了5字節未使用的空間.

因爲保存了字符串的長度,所以redis獲取字符串的長度時間複雜度爲O(1),同時可以杜絕緩衝區溢出,當需要修改SDS時,會先檢查空間是否足夠,不夠的話會先擴展空間再保存新的數據.

通過buf中的free表示的未使用空間,SDS實現了空間預分配和惰性空間釋放兩種優化策略.

1.1空間預分配

當進行字符串增長操作,需要對SDS進行空間擴展時,程序會在分配修改所需的必要空間之外,再分配額外的未使用空間,分配策略如下:

如果修改之後SDS的長度小於1MB,那麼程序將分配和修改之後len長度同樣大小的未使用空間;如果修改之後的SDS長度大於1MB,那麼

程序將分配1MB的未使用空間.

1.2惰性空間釋放

當進行字符串縮短操作時,修改後空餘出來的空間並不會被立即釋放,而是記錄在free中,當下次進行字符串的擴展時,如果字符串長度小

於free的值,就不需要進行空間擴展操作,通過這個策略避免了既避免了縮短字符串之後的內存重分配操作,又爲將來的拓展留出空間,同

時SDS提供了專門釋放空間的api,不需要擔心free空間太大造成的內存浪費.

以下是SDS字符串與C字符串的區別:

 

二.鏈表

鏈表在redis中的應用比較廣泛,list類型的值對象底層實現之一就是鏈表,當列表鍵中包含了數量比較多的元素,或者包含的元素都是比較

長的字符串時,就會使用鏈表(後面筆記中記錄),除此之外發布與訂閱,慢查詢,監視器等功能也用到了鏈表,Redis服務器本身還用鏈表來保

存多個客戶端的狀態信息以及構建客戶端輸出緩衝區.

redis的鏈表實現是雙端無環鏈表,其結構與示意圖如下:

此外還通過list結構對鏈表進行持有;示意圖及結構如下:

三.字典

字典中一個key對應一個value,每個key是唯一的,redis數據庫底層就是使用字典實現,增刪改查操作也是建立在對字典的操作之上.除此

之外,字典還是值對象類型爲hash時的底層實現,當一個hash值對象包含的數據比較多或者包含的數據的長度都比較長的時候,redis會使

用字典作爲其底層實現,而字典的底層又是使用哈希表實現,每個哈希表包含多個哈希節點每個哈希節點保存了字典中的一個鍵值對

redis哈希表使用鏈地址法解決哈希衝突,多個衝突的節點通過next指針相鏈接,當有衝突時,新的節點放在其他節點的前面.當hash表中

數據過多或者過少時,會通過rehash來重新分配空間(將原來小空間哈希表上的節點rehash保存到另外一個大空間哈希表上的,之後將原

來的小表置空).

3.1哈希表的擴展與收縮

當以下條件中的任意一個滿足時,程序會自動開始對hash表進行擴展操作:

1)服務器目前沒有執行BGSAVE或者BGREWRITEAOF命令,且韓系表的負載因子大於等於1(負載因子=已保存的節點數量/哈希表大小)

2)服務器正在執行以上兩個命令中的一個,但是負載因子大於等於5

當哈希表負載因子小於0.1,程序自動開始對哈希表執行收縮操作.

3.2漸進式rehash

當擴展或收縮哈希表的時候,需要對其中保存的鍵值對進行rehash,但是爲了表面對服務器性能造成影響,並不是一次性rehash全部鍵值

對而是分多次漸進式分配.漸進式rehash期間新增的鍵值對不會保存到老的哈希表中,會直接進入新hash表.

 

四.跳錶

redis使用跳錶(skiplist)作爲zset對象類型的底層實現之一:當一個zset包含的元素數量比較多或者包含的成員都是比較長的字符串時

redis中只在兩個地方使用了跳錶,一個是實現zset數據類型,另外一個是在集羣節點中用作內部數據結構.同一個跳躍表中,各個節點保存

的成員對象必須是唯一的,但是分值可以相同,分值相同時按照字典排序小的在前大的在後.

 

五.整數集合

整數集合(intset)值對象爲set數據類型的底層實現之一,當一個集合質包含整數值元素,並且集合的元素數量不多時.

intset是redis用來保存整數值的集合抽象數據結構,可以保存類型爲int16_t,int32_t或者int64_t的整數值,並且保證不會出現重複:

整數集合數據結構如下:

typedef struct intset{
    //編碼方式
    uint32_t encoding;
    //集合包含的元素數量
    uint32_t length;
    //保存元素的數組
    int8_t contents[];
}intset

contents[]用於保存集合中的元素(按照值的大小從小到大,不重複)

encoding中保存的編碼方式決定了contents[]中保存的值類型,有三種對應關係,encoding(content)->INTSET_ENC_INT16(int16_t):可保存-32768~32767;INTSET_ENC_INT32(int32_t)可保存-2147483648~2147283647,INTSET_ENC_INT64(int64_t)可保存-9223372036854775808~9223372036854775807

升級

當要將一個新元素添加到整數集合裏時,如果新元素的類型比整數集合現有的所有元素的類型都長,整數集合會先進行升級,然後纔將新元素添加進去,比如現有三個是int16_t,現在要添加一個int32_t會先將之前的三個轉換爲int32_t,然後再加入新元素.升級的好處一是提升整數集合的靈活性,另外能夠節約內存空間,目前暫不支持降級\

六.壓縮列表

壓縮列表(ziplist)是list和hash值對象的底層實現之一,當一個list只包含少數元素,並且每個元素要麼是小整數值要麼是長度比較短的

字符串,或者一個hash鍵的值包含少量的鍵值對,並且每個鍵值對要麼要麼是小整數值要麼是長度比較短的字符串.

壓縮列表是redis爲了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構,一個壓縮列表可以包含任意多

個節點(entry),每個節點可以保存一個字節數組或者一個整數值.

七.對象

7.1對象類型與編碼

Redis使用對象來保存數據庫中的鍵和值,分別爲鍵對象和值對象.每個對象由一個redisObject結構表示,該結構中和保存數據有關的

三個屬性分別是type,encoding和ptr:

typedef struct redisObject{
    //類型
    unsigned type:4;
    //編碼
    unsigned encoding:4;
    //指向底層實現數據結構的指針
    void *ptr;
    //...
}

對於redis數據庫保存的鍵值對來說,鍵總是一個字符串對象,值可以是字符串對象,list對象,hash對象,set對象和zset對象.可以使用

tape命令查看值對象的類型,命令格式爲type key.

對象的ptr指針指向對象的底層實現數據結構,由對象的encoding屬性決定,encoding屬性記錄了對象的底層實現,這個屬性的值可以

是下表中列出的常量中的一個:

對象的編碼
編碼常量 編碼所對應的底層數據結構
REDIS_ENCODING_INT                          long類型的整數
REDIS_ENCODING_EMBSTR  embstr編碼的簡單動態字符串
REDIS_ENCODING_RAW 簡單動態字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 雙端鏈表
REDIS_ENCODING_ZIPLIST 壓縮列表
REDIS_ENCODING_INTSET  整數集合
REDIS_ENCODING_SKIPLIST 跳躍表和字典

每種類型的對象都至少使用了兩種類型的編碼,如下表所示:

不同類型和編碼的對象
類型 編碼 對象
REDIS_STRING REDIS_ENCODING_INT 使用整數值實現的字符對象
REDIS_STRING REDIS_ENCODING_EMBSTR 使用embstr編碼的簡單動態字符串實現的字符串對象
REDIS_STRING REDIS_ENCODING_RAW 使用簡單動態字符串實現的字符串對象
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的list對象
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用雙端鏈表實現的list對象
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的hash對象
REDIS_HASH REDIS_ENCODING_HT 使用字典實現的hash對象
REDIS_SET REDIS_ENCODING_INTSET 使用整數集合實現的set對象
REDIS_SET REDIS_ENCODING_HT 使用字典實現的set對象
REDIS_ZET REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的zset對象
REDIS_ZET REDIS_ENCODING_SKIPLIST 使用跳躍表和字典實現的zset對象

使用object encoding key命令可以查看對應鍵的值對象的編碼,每種值對象至少使用兩種編碼可以方便redis在不同的場景下選擇合

適的數據實現,提高效率.

7.2字符串對象

字符串對象的編碼可以是int,raw或者embstr.,各自使用場景如下:

int:   當字符串對象保存的是整數值,並且該整數值可以用long類型表示

raw:  當保存的是字符串並且該字符串長度大於39字節,使用一個SDS來保存值,並將編碼設置爲raw

embstr:   當保存的是字符串並且該字符串長度小於39字節,將編碼設置爲embstr

embstr編碼是專門用於保存短字符串的一種優化編碼放是,其和raw編碼一樣,都是用redisObject結構和sdshdr結構來表示字符串對

象,但raw編碼需要調用兩次內存分配函數來創建redisObject結構和sdshdr結構,而embstr通過調用一次內存分配函數來分配一塊連

續的空間,空間中依次包含redisObject結構和sdshdr結構:

使用embstr編碼好處是:1.降低內存分配次數(從兩次降低爲一次)2.釋放字符串對象時,raw需要調用兩次內存釋放函數,embstr只

需要一次3.因爲embstr編碼對象保存在連續空間中,能更好利用緩存帶來的優勢

可以用long double類型表示的浮點數在redis中也是作爲字符串保存的.編碼對象可以相互轉換,其中embstr編碼沒有任何修改程序,因此是隻讀的.下表是常見字符串命令的實現:

7.3 list對象

list對象的編碼可以是ziplist或者linkedlist,當list對象同時滿足以下兩個條件時,使用ziplist編碼,否則使用linkedlist

1.list對象保存的所有字符串元素的長度都小於64字節;

2.元素數量小於512個

以上兩個數值在配置文件中可以修改,對應的是list-max-ziplist-value選項和list-max-ziplist-entries

7.4 hash對象

hash對象的編碼可以是ziplist或者hashtable,當使用ziplist壓縮列表編碼時,一個鍵值對分別保存在一個節點上,一前一後緊挨在一

起.先添加的鍵值對在表頭方向,後添加的在表尾方向;當使用hashtable編碼時,底層使用字典作爲實現,鍵值對都是字符串對象.hash

對象同時滿足以下兩個條件時使用ziplist編碼,否則使用hashtable編碼:

1.鍵值對的鍵和值字符串長度都小於64字節;

2.鍵值對個數小於512個

以上兩個數值在配置文件中可以修改,對應的是hash-max-ziplist-value選項和hash-max-ziplist-entries

7.5 set對象

set對象的編碼可以是intset或者hashtable,當對象同時滿足以下兩個條件是,使用intset編碼,否則使用hashtable編碼:

1.保存的所有元素都是整數值

2.元素的數量不超過512個

第二個數值在配置文件中可以修改,對應的是set-max-intset-entries

7.6 zset對象

zset對象的編碼可以是ziplist或者skiplist.當使用ziplist編碼時,每個元素使用兩個緊挨在一起的壓縮列表節點來保存,第一個節點保

存元素的成員(member),第二個節點保存元素的分值(score),列表內按照分值從小到大排序,分值小的在表頭方向,分值大的在表尾方

向,skiplist編碼看p78.當同時滿足以下兩個條件時,使用ziplist編碼,否則使用ziplist編碼:

1.元素數量個數少於128個

2.所有元素成員的長度都小於64字節

以上兩個數值在配置文件中可以修改,對應的是zset-max-ziplist-entries選項和zset-max-ziplist-value

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章