Redis-庫之數據結構

一、簡介

Redis數據庫裏邊的每一個鍵值對(key-value pair)都是由對象構成。其中,數據庫鍵總是一個字符串對象(sting object),而值則可能是字符串對象(string objec)、哈希對象(hash object)、列表對象(list object)、集合對象(set object)、有序集合(sorted set object)的其中一種。

這些鍵值對象,都是由底層redis內置定義的各種數據結構所支持組成的,本文意在講解redis對象的底層數據結構原理,以及一些由於存儲對象的變化所帶來的底層數據結構的變更,從而有目的有方向的幫助我們更加專業的使用redis,性能、效率達到最大。

二、概況

我們知道redis支持5種對象結構供用戶使用,它們分別是:

數據類型

數據類型TYPE

字符串對象(sting object) REDIS_STRING
列表對象(list object) REDIS_LIST
哈希對象(hash object) REDIS_HASH
集合對象(set object) REDIS_SET
有序集合(sorted set object) REDIS_ZSET

redis在底層提供了8種基礎數據結構,用來支持在應用層面給用戶提供的這5種對象結構,底層數據結構標識如下:

底層編碼TYPE 底層數據結構
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中提供的5種對象結構,是由上述8中底層數據結構中的一種或者多種構成組成的,下面我們詳細的介紹這8中基礎數據結構的工作原理及其特性。

三、詳情

3.1 簡單動態字符串(SDS)

簡單動態字符串(simple dynamic string)是redis的字符串的默認表示方式,如果是表示字符串字面量,比如一些無需修改的字符串,則會使用C語言的字符串表示,比如日誌打印;如果是動態的字符串表示,則會使用SDS結構來存儲表示。

使用場景

SDS是redis最底層最基本的數據結構,在redis的數據庫中包含字符串值的鍵值對都是使用SDS存儲的。

結構定義

redis中SDS的結構定義爲:

struct sdshdr {
    //buf數組已使用字節數量;SDS保存字符串長度
    int len;
    //buf數組空閒的字節數量
    int free;
    //存儲數據的數組
    char buf[];
}

一個保存數據的SDS數據結構的例子如下所示:

上圖展示的是一個存儲着redis字符串的SDS結構示例:

len:數值爲5,表示這個SDS存儲的字符串的長度爲5;

free:數值爲1,表示這個SDS結構還可以存儲的字節數,當前還有1字節可用;

buf:是實際存儲字符串的char類型的數組,以‘\0’表示結束。

注:'\0'空字符結尾遵循了C語言字符串的規範,這個空字符的1字節不計算在SDS結構的長度內,遵循這個規範可以支持直接使用C語言的一部分函數功能。

與C對比

C語言的字符串存儲信息,沒有記錄當前存儲的字符串長度信息,相對於SDS中的len屬性,因此SDS將獲取字符串長度操作複雜度從O(N)降低到O(1);另外由於C沒有記錄長度信息,因此一些操作比如拼接,會有緩衝區溢出的風險。SDS使用free和len屬性解除數組和字符串長度的關聯,會根據free檢查新增的字符串是否超長,進行動態擴容操作。

SDS是以二進制的方式來處理存放在buf數組中的數據,不會對數據進行任何額外操作、過濾和假設等操作,也就是存進去是什麼樣子,拿出來的時候也是一樣。

內存策略

1.空間預分配:用於優化SDS字符串增長操作,以修改後的len屬性值和閾值1MB比較

如果len < 1MB,擴容至新長度的2倍,即如果新修改的字符串是15字節,則擴容至free=15字節,一共30+1字節;

如果len >= 1MB,則分配1MB的使用空間,過程如下:

2.惰性空間:用於優化字符串縮短操作

當SDS字符串進行縮短操作時,SDS並不會直接釋放掉已經分配的內存,而是記錄在free中,用於下一次增長字符串操作使用,避免了內存重分配的資源消耗。當然會有造成內存泄漏的風險,SDS提供了釋放未使用空間的api,避免這種問題。

總結

簡單動態字符串和C語言的字符串相比有幾個優點如下:

C語言字符串 SDS
獲取字符串長度複雜度O(N) 獲取字符串長度複雜度O(1),len屬性獲取
api操作內存溢出風險 規避內存溢出風險,動態擴容
只能存儲文本字符串 可以存儲文本字符串、二進制數據
修改字符串N次,必然需要N次內存重分配 修改字符串N次,最多需要N次內存重分配(預分配)
可以使用本身所有操作函數 可以使用一部分C語言庫函數(保留空字符結尾)

3.2 鏈表(linked list)

鏈表具有高效的節點重排能力,並且可以順序訪問,通過節點操作來改變整個鏈表的狀態。在C語言內無內置鏈表結構,因此redis構建了自己的鏈表數據結構。

使用場景

列表鍵元素較多的時候,底層就會使用鏈表來存儲元素,以及在發佈訂閱、慢查詢、監視器等地方也用到了鏈表結構。

結構定義

鏈表是由兩個數據結構定義而成的,首先是節點組成結構,listNode邏輯如下:

struct listNode {
    //前置節點
    struct listNode *prev;
    //後置節點
    struct listNode *next;
    //節點值
    void *value;
}

上述結構可以看到,前置後置節點指針組成一個雙向的鏈表,redis還使用了另外一個結構持有此鏈表,操作更加方便,list結構如下:

struct list {
    //頭節點
    listNode *head;
    //尾節點
    listNode *tail;
    //鏈表節點數量
    unsigned long len;
    //節點值複製函數
    void *(*dup)(void *ptr);
    //節點值釋放函數    
    void (*free)(void *ptr);
    //節點值對比函數
    void (*match)(void *ptr, void*key);
}

 可以看到list結構定義了listNode的首尾節點以及鏈表節點數量,可以更加方便的操作管理listNode節點,整個鏈表結構如下:

特性

特性 概括
雙端 每個listNode節點包含前置和後置節點,獲取前後節點複雜度O(1)
無環 頭尾節點的前驅和後置爲null,作爲鏈表訪問終點
頭尾指針 list結構分別指向listNode頭尾節點,獲取頭尾節點複雜度O(1)
長度計數 獲取鏈表長度複雜度O(1)
多態 void*指針指向任意類型數據,提供操作節點函數

3.3 字典(dict)

字典又被稱作符號表(symbol table)、關聯數組(associative array)或者映射(map),是一種用來存儲key-value鍵值對的抽象數據結構。在一個字典裏邊每個key值都是唯一的,通過key值查找到對應的value值。

使用場景

redis數據庫使用字典結構實現,另外字典也是哈希鍵的實現之一,當哈希鍵的元素比較多或者元素比較大的時候,就會使用字典結構來作爲哈希鍵底層的存儲結構。

結構定義

字典使用哈希表作爲底層實現,首先我們看下哈希表的結構定義,dictht結構如下:

struct dictht {
    //哈希表數組
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩碼,計算索引,等於size-1
    unsigned long sizeMask;
    //哈希表已有節點數量
    unsigned long used
}

table是一個dictEntry結構的數組,裏邊存儲着指向dictEntry結構的一個指針,size是哈希表的大小,used是哈希表已經使用的節點數量,sizeMask是用來計算元素節點在table數組的位置。下面我們看下哈希表節點dictEntry結構定義:

struct dictEntry {
    //哈希表節點的key值
    void *key
    //哈希表節點的value值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    //指向下一個哈希表節點,形成鏈表
    struct dictEntry *next;
}

可以看到,每一個哈希表節點dictEntry存有一個key和一個value,以及一個指向下一個哈希表節點的指針,這樣就可以形成一個鏈表,在table相同位置的哈希表節點則會通過這個指針進行關聯。在redis中,字典是使用dict結構來表示字典的,結構如下:

struct dict {
    //類型特定函數
    dictType *type;
    //私有數據
    void *privdata;
    //哈希表
    dictht ht[2];
    //rehash索引(rehash不在進行時,值爲-1)
    int trehashidx;
}

因此可以看出,redis中的字典是以哈希表作爲基礎實現的,通過dict進行哈希表的關聯管理,默認的dict初始化了dictht數組的長度爲2,哈希表是由dictht哈希表和dictEntry哈希表節點所定義的,假設當存儲部分數據之後,結構如下:

哈希算法

dict默認初始化dictht哈希表數組的長度爲2,默認使用ht[0]哈希表,ht[1]是最爲rehash時使用的一個空間哈希表。當一個新的元素添加到字典裏邊時,基本思想是根據hash算法計算出新元素在dictht[x]中table數組的位置,計算過程如下:

//根據設定的hashtype找到設定的hash函數,計算key的hash值
hash = dict->type->hashFunction(key)
//key的hash值和哈希掩碼所與運算計算下標誌
index = hash & dict->ht[x].sizemask

假設現在有一個字典是空的,我們將(k0,v0)假如到字典中,大致過程如下:

計算k0的哈希值,我們假設hash = dict->type->hashFunction(k0)值爲8,在通過index = hash & dict->ht[x].sizemask計算得到index=8 & 3 = 0;

哈希衝突

哈希表解決哈希衝突是使用的鏈地址法,我們前面介紹到dictEntry中含有下一個哈希表節點的指針next,同一鏈關聯的節點在table中處於同一個下標;並且由於dictht沒有尾節點的指針,因此爲了速度考慮,新的節點總是處於鏈表的頭節點上。

rehash

隨着哈希表的節點的數量的變化,爲了哈希表的負載因子維持在一個合理的範圍內,我們需要對哈希表進行必要的擴展和收縮,也就是對哈希表進行重哈希的操作。當以下條件任意一個滿足的時候,則會主動的進行rehash動作:

1.服務器目前沒有執行BGSAVE或者BGREWRITEAOF命令,且哈希表的負載因子大於等於1;

2.服務器目前正在執行BGSAVE或者BGREWRITEAOF命令,且哈希表的負載因子大於等於5;

負載因子 = ht[0].used / ht[0].size,之所以在執行復制命令期間提高負載因子的閾值,是因爲執行BGSAVE或者BGREWRITEAOF命令會fork出一個子進程,增加了系統的負荷,且需要內存。提高負載因子,從而提高rehash的閾值,最大限度節約內存。除此之外,如果負載因子小於0.1,程序會自動對哈希表執行收縮動作。

重哈希的操作過程如下:

1.爲ht[1]分配空間,這個空間的大小取決於ht[0]包含的元素數量也就是ht[0]的used屬性值和要執行的操作;

2.如果是擴展操作,則ht[1]的大小是第一個大於等於ht[0].used*2的2的n次方冪;

3.如果是收縮操作,則ht[1]的大小是第一個大於等於ht[0].used的2的n次方冪;

4.ht[1]內存空間分配好後,將ht[0]上的節點rehash到ht[1]上面,也就是將原來哈希節點通過key的新hash計算出新的index,並放入到ht[1]中指定位置;

5.ht[0]上所有的鍵值對全部遷移至ht[1]後,則會把ht[0]釋放,並將ht[1]更改成ht[0],且新創建一個ht[1]的哈希表作爲下一次的rehash使用;

問題?如果哈希表很大的話,大量的遷移操作會影響服務性能,以及在轉移期間發生的新的鍵的變更該如何處理呢?在redis中通過漸進式哈希方式解決這些問題,漸進式哈希過程如下:

1.爲ht[1]分配空間,這個空間的大小取決於ht[0]包含的元素數量也就是ht[0]的used屬性值和要執行的操作;

2.在字典中維持了一個rehashidx的屬性,初始化爲0,表示rehash正式開始;

3.在rehash進行期間,每次對字典執行ADUS操作時,除了執行指定操作外,還要將ht[0]表上索引爲rehashidx上的鍵值對rehash到ht[1]上,全部哈希完畢後,將rehashidx值加一;

4.隨着操作不斷執行,最終在某個時間點,ht[0]全部的元素節點rehash到了ht[1]上,此時將rehashidx設置爲-1,完成。

漸進式哈希避免了短時間大量的遷移工作和計算量影響服務性能。在漸進式rehash期間,ht[0]和ht[1]是共同工作的,ADUS操作都是在二者一起進行的,其中新增工作只會在ht[1]上進行,查詢則會先去ht[0]查詢,如果不存在則會到ht[1]再次查詢,隨着時間和操作的執行,最終ht[0]會變成一個空表。

3.4 跳躍表(skip list)

跳躍表是一種有序數據結構,它通過每個節點中維持了多個指向其他節點的指針,從而達到快速訪問節點的目的。大多數情況下跳躍表查找平均複雜度是O(logN),最壞情況是O(N)。跳躍表的效率可以和平衡樹媲美,並且因爲跳躍表的實現簡單,很多情況下可以採用跳躍表來代替平衡樹。

使用場景

跳躍表是有序集合的底層數據結構之一,如果一個有序集合的元素數量比較多或者有序集合中元素的長度比較大,則會使用跳躍表作爲底層的數據存儲結構。

結構定義

跳躍表由兩個結構定義的,首先是zskiplist結構用於保存跳躍表相關信息,zskiplistNode是跳躍表的節點表示,首先我們看下跳跳躍表節點,zskiplistNode結構定義:

struct zskiplistNode {
    //後退指針
    struct zskiplistNode *backward;
    //分值
    double score;
    //成員對象
    robj *obj;
    //層
    struct zskiplistLevel {
        //前進指針
        struct zskiplistNode *forward;
        //跨度
        unsigned int span;
    } level[];
}

層:zskiplistNode的層數組可以包含多個前進指針,指向不同的跨度的節點,一般來說前進指針越多,訪問其他節點越快;每次新創建一個zskiplistNode節點的時候都會根據冪次定律(越大的樹出現機率越小)隨機生成一個介於1和32之間的數值作爲level數組的大小。

前進指針: 每個節點有多個層,每一個層都有一個前進指針,用於從表頭訪問表尾方向的節點;

後退指針:用於表尾向表頭方向訪問節點,和前進指針不同的是,每一個節點只有一個表尾指針;

分值:每一個節點都存儲着一個分值,double類型的浮點數,跳躍表中數據都按照分值進行排序,相同分值的按照對象字典序排序;

對象:對象是節點存儲的一個對象指針,他指向了一個字符串對象,字符串對象則保存着一個SDS值;

zskiplist維護管理着這些節點,可以更方便的對跳躍表節點進行處理,例如快速訪問頭、尾節點,快速獲取跳躍表長度以及最大深度,下面我們看下zskiplist結構定義:

struct zskiplist {
    //跳躍表頭尾節點指針
    struct zskiplistNode *header, *tail;
    //跳躍表節點數量
    unsigned long length;
    //跳躍表最高層數
    unsigned long level;
}

首先定義了頭尾指針指向了跳躍表的頭和尾,另外記錄了跳躍表的節點數量還有最大層數節點的層數,下面我們看下具有節點的跳躍表的基本結構樣式:

 跳躍表的查找過程是自上而下的一個跨度查找,先從上層確認區間,然後逐級下查,平均複雜度爲O(logN)。

3.5 整數集合(int set)

整數集合是redis 用來保存整數值的數據結構,可以保存類型爲int16_t、int32_t或者int_64的整數值,並且保證集合中不會出現重複數字。

使用場景

整數集合是集合鍵的底層實現之一,當一個集合鍵只包含整數值元素,並且這個集合元素不多的時候,就會使用整數集合作爲底層實現。

結構定義

在redis中使用intset結構表示整數集合,intset結構如下:

struct intset {
    //編碼方式
    unit32_t encoding;
    //集合元素數量
    unit32_t length;
    //底層數組
    int8_t contents[];
}

 encoding表示contents數組的編碼方式,包括int_16、int_32以及int_64;length屬性表示數組中元素的數量;contents數組是實際存儲元素的地方,雖然聲明爲int8類型,但是他是按照encoding編碼類型存儲數據,數組中的元素按照從小到大的順序排序,並且數組中沒有重複整數。

當encoding=INTSET_ENC_INT16時,則數組中的值都是int16_t的整數值(-32768~32767),同理當encoding=INTSET_ENC_INT32或者encoding=INTSET_ENC_INT64時候分別定義了數組中整數的類型,下圖展示了一個存儲元素的整數集合的示例:

升級

當已有集合存儲的是int16_t類型的數據,如果此時有一個int32_t類型的整數存儲進來的時候,就涉及到整數集合的升級邏輯,因爲原來的存儲方式無法滿足新的整數的存儲。整數集合的升級操作分爲3個步驟:

1.首先我們根據新整數類型進行數組的擴容空間操作,爲新元素分配空間;

2.將底層元素按照新的編碼方式進行轉換,並且安放在新的位置上,且保證有序性不變;

3.將新元素添加到數組中,完畢。

下圖展示了添加新元素需要升級的例子:

首先看一下初始狀態,inset裏邊有是那個元素,1,2,3,按照元素11以int16_t編碼格式存儲,如下圖:

當新元素65535以int32_t編碼格式添加至此整數集合中時,則當前intset需要進行升級操作,先按照新編碼格式進行擴容操作:

接下來就是將原來的元素按照新的編碼方式轉化並放入對應位置,比如原來的元素3,在四個元素中排行第三,則應放入新的數組的索引2處:

如此類推,按照排序:3->index2;2->index1;1->index0,移動完畢後,情況如下:
 

最後我們把新增的元素放到數組中,位置是索引3,且把length和encoding屬性進行更新:

到此,intset整數集合的升級操作完成,升級操作區分了編碼方式,這樣可以使得同一個數組存儲相同類型數據,避免類型錯誤;另外,比全部直接使用int64_t類型存儲方式更加節省內存。

注:一旦升級,就不會在降級,就會一隻保持升級之後的編碼方式,哪怕是後來只有低級的編碼類型。

3.6 壓縮列表(ziplist)

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

使用場景

壓縮列表是列表鍵和哈希鍵實現的底層結構之一,當一個列表鍵包含比較少的列表項,並且每個列表項是比較小的字符串或者小整數值,這個列表鍵底層就是使用壓縮列表作爲實現結構。

結構定義

壓縮列表的結構如下:

各個參數的含義如下:

屬性 類型 長度 用途
zlbytes uint32_t 4字節 記錄了整個壓縮列表所佔的內存字節數:在對壓縮列表進行內存重分配或者計算zlend位置使用
zltail uint32_t 4字節 記錄了列表的表尾節點距離列表起始地址的字節數:通過此偏移量可以不用遍歷這個那個列表就知道列表尾節點的位置
zllen uint16_t 2字節

記錄了壓縮列表的節點的數量:當整個屬性值小於uint16_max(65535)時,這個屬性就是列表的節點數量;如果超過這個值,則需要遍歷整個列表才知道節點數量

entry 列表節點 不定 壓縮列表的各個節點,節點長度根據節點內容而定
zlend uint8_t 1字節 特殊值0xFF(十進制255),用於標記壓縮列表的結尾

下圖展示了一個包含三個節點的壓縮列表示例,壓縮列表佔用字節0x50(十進制80),尾節點距離起始地址0x3c(十進制60),因此p表示頭,則p+0x3c表示尾部節點,情況如下:

entry壓縮列表節點可以保存一個字節數組或者一個整數值,其中字節數組可以是以下三種長度之一:

1.長度小於等於63(2的6次冪減1)字節的字節數組;

2.長度小於等於16383(2的14次冪減1)字節的字節數組;

3.長度小於等於4294967295(2的32次冪減1)字節的字節數組;

整數值可以是以下六種長度之一:

1.4位長,介於0至12之間的無符號整數;

2.1字節長的有符號整數;

3.3字節長的有符號整數;

4.uint16_t類型整數;

5.uint32_t類型整數;

6.uint64_t類型整數;

每一個entry結構都是由三個屬性組成,entry結構如下圖:

previous_wntry_length表示前一個節點所佔的字節數,也就是前一個entry的長度,因此可以通過一個節點的起始地址,便可以通過與previous_wntry_length屬性向前遍歷整個列表節點。本屬性的長度爲1字節或者5字節,如果前一個節點的長度小於254字節,則此屬性是1字節,如果前節點長度超過254字節,則此屬性是5字節長,其中第一字節設置爲0xFE(十進制254),後面的四個字節用來保存前節點的長度。0x05表示前節點長度是5字節,0xFE00002726表示前節點長度爲0x00002726(十進制10086)

encoding記錄了content保存的數據的類型以及長度,也就是剛纔上述提到的節點可取類型3種數組+6種整數類型;

1.一字節、兩字節和五字節長,值得最高位是00、01和10是字節數組編碼,表示content存儲的是字節數組,數組的長度在encoding去除最高兩位的其他位表示;

2.一字節長,值得最高位是11開頭的整數編碼,表示content存儲的是整數值,整數的長度在encoding去除最高兩位的其他位表示;

content屬性保存了節點存儲的值,可以是整數或者數組,由encoding屬性決定。

下圖分別展示了存儲字節數字和整數值的壓縮列表的示例:

連鎖更新問題

前面介紹壓縮列表使用previous_entry_length屬性記錄前一個節點所佔的字節數,前節點長度是否超出254字節,決定了本節點的previous_entry_length屬性所佔的長度是1還是5字節。現在有這樣一個場景,我們假設有e1到eN的壓縮列表節點,他們的長度都介於250~253字節之間,這樣每個節點的previous_entry_length屬性佔用1字節,這時候如果新來了一個節點插入到列表頭部,他的長度大於等於254字節,此時後面的節點就需要重新分配previous_entry_length屬性至5個字節來保存新節點的長度,那麼問題就來了,後節點發現前面超出254字節了,他也要更新previous_entry_length屬性長度爲5字節,循環到尾部,導致了連鎖更新的問題,當然刪除節點也會產生這樣問題。

連鎖更新最壞需要N次內存重分配,複雜度還是比較高的,但是連鎖更新發生的情況也是比較極端的,需要好多連續介於250~253字節長度的節點,這種不是很常見。

四、應用

前面介紹了redis底層定義的幾種數據結構,包括簡單字符串(SDS)、鏈表(linked list)、字典(dict)、壓縮列表(ziplist)、跳躍表(skiplist)、整數集合(intset)。

redis數據庫沒有直接使用這些底層的數據結構,而是基於這些底層的數據結構構建了對象系統,每種對象至少包含了我們在前面介紹的一種數據結構。在上層使用對象結構可以針對不同的使用場景使用不同的底層數據結構,從而優化對象在不同場景下的使用效率。

redis使用對象表示數據庫中的鍵值對,每當我們創建一個鍵值對的時候,我們至少創建倆個對象,一個是鍵對象,另一個是值對象。對象結構是使用redisObject結構定義的,結構如下:

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

type屬性記錄了這個redisObject的類型,也就是文章開頭提到的,提供給客戶端使用的五種redis對象類型:

數據類型

數據類型TYPE

type命令對應輸出
字符串對象(sting object) REDIS_STRING string
列表對象(list object) REDIS_LIST list
哈希對象(hash object) REDIS_HASH hash
集合對象(set object) REDIS_SET set
有序集合(sorted set object) REDIS_ZSET zset

當我們對一個鍵執行type命令時,將會返回這五種redisObject類型之一。

ptr指針指向了底層存儲的數據結構,而這些數據結構由encoding決定,也就是我們上面提到的8中底層數據結構,每一種redisObject都至少使用了兩種編碼,下面是對象和編碼的組合關係:

類型type 編碼encoding redisObject-redis對象
REDIS_STRING REDIS_ENCODING_INT 使用整數值實現的字符串對象
REDIS_STRING REDIS_ENCODING_EMBSTR 使用embstr編碼的簡單動態字符串實現的字符串對象
REDIS_STRING REDIS_ENCODING_RAW 使用簡單動態字符串實現的字符串對象
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的列表對象
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用雙端鏈表實現的列表對象
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的哈希對象
REDIS_HASH REDIS_ENCODING_HT 使用字典實現的哈希對象
REDIS_SET REDIS_ENCODING_INTSET 使用整數集合實現的集合對象
REDIS_SET REDIS_ENCODING_HT 使用字典實現的集合對象
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的有序集合對象
REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳躍表實現的有序結合對象

可以使用命令“OBJECT ENCIDING”輸出redis對象的編碼方式,下面我們看下redis五種對象機構的編碼切換方式。

1.string

上面介紹了string對象有三種編碼方式,分別是int、embstr和raw,他們的分別出現場景是:

當字符串保存的是一個整數值,並且可以用long表示時,該字符串對象使用int編碼;

當字符串編碼是一個字符串時,如果字符串的長度大於39,則使用raw編碼表示,ptr指針指向底層SDS底層結構;

當字符串編碼是一個字符串時,如果字符串的長度小於等於39,則使用embstr編碼表示,ptr指針指向底層embstr編碼的SDS底層結構;

下圖是三者的一個示例:

字符串保存的是一個整數值,可以用long表示時,該字符串對象使用int編碼

字符串編碼是一個字符串,且字符串的長度大於39,則使用raw編碼表示,ptr指針指向底層SDS底層結構 

字符串編碼是一個字符串,且字符串的長度小於等於39,則使用embstr編碼表示,ptr指針指向底層embstr編碼的SDS底層結構 ,可以看到結構是比較緊湊的,他和raw編碼方式在數據結構上是相同的,都是使用SDS簡單動態字符串此層數據結構,不同之處在與embstr只需要一個內存分配即可完成整個的數據存儲,而raw需要兩次內存分配分別構造redisObject和SDS對象。

 int和embstr在一定情況下,會改編成raw編碼方式,比如原來是int編碼,修改成了一個字符串或者拼接了字符串導致不可以用long表示,則會升級爲raw;embstr編碼方式沒有提供修改的程序,也就是說embstr是一個只讀的編碼方式,如果對embstr編碼的字符串修改,則會改變編碼方式爲raw,然後在進行修改。

2.list

列表對象的底層實現是ziplist或者linkedlist,出現的場景是:

當列表保存的字符串長度都小於64字節且節點數量小於512個的時候使用ziplist編碼,否則使用linkedlist結構;這兩個限制閾值是可以通過配置文件修改的,以下是兩種編碼方式的示例:

使用ziplist數據結構編碼的列表:

假設此列表是使用linkedlist編碼結構,則如下所示,簡化了stringObject的結構展示:

3.hash

哈希對象的結構可以是ziplist或者ht,區分場景如下:

當哈希對象保存的所有鍵值對的鍵和值的長度都小於64字節,且保存的鍵值對數量小於512個的時候使用ziplist,否則使用ht。

分別使用兩種編碼的hash結構如下:

下圖展示的是使用壓縮列表的哈希對象,可以看到鍵值對是緊緊貼在一起的,添加的時候,首先把鍵添加至壓縮列表的結尾,然後再把值添加到列表結尾,後添加的鍵值對在壓縮列表的結尾處:

下圖展示了使用ht結構的哈希對象的結構: 

 4.set

集合對象的編碼方式有intset和ht,區分場景如下:

當集合對象保存的都是整數值,且集合元素數量不超過512個,就會使用intset編碼,否則使用ht結構存儲,兩種編碼方式的集合對象機構如下圖:

下圖展示了使用intset編碼的集合對象的結構圖示:

下圖展示了使用ht編碼方式的集合對象的結構:

5.zset

有序集合使用了ziplist和skiplist兩種編碼結構,區分場景如下:

當有序結合數量小於128個,並且所有元素成員的長度都小於64字節,就會使用ziplist編碼方式,否則使用skiplist編碼,兩種編碼方式的有序集合的結構如下:

下圖展示了使用ziplist編碼方式的有序結合機構,可以看到元素是按照分值從小到大進行排列的,對象和分值是緊緊挨在一起的

下圖展示了使用skiplist編碼方式的有序集合對象,可以看到這個編碼下同時使用了ht字典和skiplist兩種底層結構,通過字典可以用O(1)的複雜度獲取到成員的分值,跳躍表本身通過分值排序的,所以有序查找更快,二者同時存在,是爲了性能得到提升,且圖中展示的成員和分值是分開展示的,實際上是公用一份的,這裏是爲了圖的區分度:

五、資源地址

官網:https://redis.io/

文檔:《Redis 設計與實現》 

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