1. 程式人生 > 其它 >redis-資料結構和物件

redis-資料結構和物件

redis面試題:https://developer.aliyun.com/article/774125

一、資料結構和物件

2、簡單動態字串(SDS, Simple Dynamic String)

在Redis的資料庫中,包含字串的鍵值對在底層都是由SDS實現的。

  1. SDS的定義
    每個sds.h/sdshdr結構表示一個SDS值:
struct sdshdr {
    // 記錄buf陣列中已使用位元組的數量,等於SDS所儲存字串的長度
    int len;
    // 記錄buf數值中未使用的位元組的數量
    int free;
    // 位元組陣列,用於儲存字串
    char buf[];
}

如下圖所示:

  • 此時len的值為5,表示這個SDS儲存了一個五位元組長的字串;
  • free的屬性值為5,表示這個SDS沒有分配、未使用的空間;
  • buf是一個char型別的陣列,陣列的前五個位元組分別儲存了'R'、'e'、'd'、'i'、's',緊接著的是一個空字元'\0',不計入len的長度,(與C)一致。
  1. SDS與C字串的區別
    1. 常數複雜度獲取字串長度:SDS中len記錄了SDS本身的長度,所以獲取一個SDS長度的時間複雜度為O(1),以此確保獲取字串長度的工作不會變成redis的瓶頸;而C是使用長度為N+1的字元陣列來表示長度為N的字串的(字元陣列的最後一個元素總是'\0'),獲取長度時,需要進行遍歷,所以時間複雜度為O(n)。

    2. 杜絕緩衝區溢位:當SDS API需要對SDS進行修改時,API會先檢查SDS的空間是否滿足修改所需的要求,如果不滿足的話,API會自動將SDS的空間擴至修改所需的大小,然後再進行操作;而C需要手動對原來的字元陣列進行操作,否則的話,就會出現溢位等操作。

    3. 減少修改字串時帶來的記憶體重分配次數:對於C來說,如果增長字串,在執行之前程式需要通過記憶體重分配來擴充套件底層陣列的空間大小,否則將產生緩衝溢位;如果縮短字串,那麼在執行過後,如果忘記通過記憶體重分配來釋放不再使用的那部分空間,將 會產生記憶體洩露。這些短板對於需要頻繁操作的redis資料庫來說是非常致命的,所以SDS要避免產生這種情況。

      1. 空間預分配:當SDS的API對一個SDS進行修改、並且需要對SDS的空間進行擴充套件的時候(在擴充套件SDS的空間之前,SDS API會先檢查未使用空間free是否足夠),程式不僅會為SDS分配修改所必須的空間,還會為SDS分配額外的未使用的空間。公式:如果對SDS修改後,SDS的len小於1MB,那麼程式分配和len屬性同樣大小的未使用空間,這時SDS的len屬性將和free屬性的值相同;如果SDS的len大於等於1MB,那麼程式將會分配1MB的未使用空間(free)。
      2. 惰性空間釋放:當SDS的API需要縮短SDS儲存的字串時,程式並不立即使用記憶體重新分配來回收縮短後多出來的位元組,而是使用free屬性將這些位元組的數量記錄起來,並等待使用。
    4. 二進位制安全:C字串中的字元必須符合某種編碼(比如ASCII),並且除了字串的末尾之外,字串裡面不能包含空字元,否則字串中間的空字元會被誤認為是字串的結尾,這些限制使得C只能儲存文字資料,而不能儲存像圖片、音訊、視訊、壓縮檔案等這樣的二進位制檔案;而SDS沒有這樣的限制,所以稱上述的buf為位元組陣列,可以存任意格式的二進位制資料。

    5. SDS相容部分C字串的操作函式。

3、連結串列

  1. 連結串列被廣泛用於實現Redis的各種功能,比如列表鍵、釋出與訂閱、慢查詢、監視器等;除此,Redis中列表鍵的實現之一就是連結串列。
    主要資料結構:
     // 連結串列節點的結構,adlist.h/listNode 
    typedef struct listNode {
        // 前置節點
        struct listNode *prev;
        // 後置節點
        struct listNode *next;
        // 節點的值
        void *value
    }listNode;
     // 連結串列的實現,adlist.h/list
    typedef struct list {
     //    表頭節點
        listNode *head;
     //    表尾節點
        listNode *tail;
     //    連結串列所含的節點數量
        unsigned long len;
     //    dup函式用於複製連結串列節點所儲存的值
        void *(*dup)(void *ptr);
     //    free用於釋放連結串列節點所儲存的值
        void (*free)(void *ptr);
     //    match用於對比連結串列節點所儲存的值和另一個輸入值是否相等
        int (*match)(void *ptr, void *key);
    }list;
    
  2. Redis連結串列實現的特性
    1. 雙端:連結串列節點帶有prev和next指標,獲取某個節點的前置節點和後置節點的複雜度都是O(1);
    2. 無環:表頭節點的prev和表尾節點的next指標都指向NULL,對連結串列的訪問以NULL為終點;
    3. 帶表頭指標和表尾指標:通過list結構的head指標和tail指標,可以以時間複雜度為O(1)去獲取連結串列的表頭表尾節點;
    4. 帶連結串列長度計數器:程式使用list結構的len屬性對list持有的連結串列節點進行計數,程式獲取連結串列中節點數量的複雜度為O(1);
    5. 多型:連結串列節點使用void* 指標來儲存節點值,並且可以通過list結構的dup、free、match三個屬性為節點值設定型別特定函式,所以連結串列可以用於儲存不同型別的值。

4、字典

字典中的每個鍵都是獨一無二的,可以通過鍵對值進行查詢、更新、刪除等操作;Redis的資料庫就是使用字典來作為底層實現的,對資料庫的增、刪、查、改操作也是構建在對字典的操作之上。除此,字典還是雜湊鍵的底層實現之一,當一個雜湊鍵包含的鍵值對比較多,又或者鍵值對中的元素都是比較長的字串時,Redis就會使用字典作為雜湊鍵的底層實現。

  1. 字典的實現:Redis的字典使用雜湊表作為底層實現,一個雜湊表裡面可以有多個雜湊表節點,每個雜湊表節點就儲存了字典中的一個鍵值對。

    1. 雜湊表dictht:
    typedef struct dictht {
        // table是一個數組,陣列中的每個元素都是一個指向dict.h/dictEntry結構的指標,
        // 每個dictEntry結構儲存著一個鍵值對
        dictEntry **table;
        // size記錄了雜湊表的大小,也即是table陣列的大小
        unsigned long size;
        // sizemask屬性的值總是等於size-1,這個屬性和雜湊值一起決定一個鍵應該被放到table陣列的哪個索引上面
        unsigned long sizemask;
        // used記錄了雜湊表目前已有節點(鍵值對)的數量
        unsigned long used;
    } dictht;
    
    1. 雜湊表節點使用dictEntry結構表示,每個dictEntry結構都儲存著一個鍵值對:
    typedef struct dictEntry {
        // 儲存著鍵值對的鍵
        void *key;
        // 儲存著鍵值對中的值,其中鍵值對的值可以是一個指標,可以是一個uint64_t整數
        // 又或者是一個int64_t整數。
        union {
            void *val;
            uint64_tu64;
            int64_ts64;
        } v;
        // next屬性是指向另一個雜湊表節點的指標,這個指標可以將多個雜湊值相同的鍵值對連線
        // 在一起,依次來解決鍵衝突問題
        struct dictEntry *next;
    } dictEntry;
    


    3. 字典,由dict.h/dict結構表示:

    typedef struct dict {
        // type屬性和privatedata屬性是針對不同型別的鍵值對,為建立多型字典而設定的
        // type屬性是一個指向dictType結構的指標,每個dictType結構儲存了一簇用於操作特定型別鍵值對的函式,
        // Redis會為用途不同的字典設定不同的型別特定函式
        dictType *type;
        // privatedata屬性是針對不同型別的鍵值對,為建立多型字典而設定的。
        void *privatedata;
        // ht是一個包含兩個項的陣列,陣列中的每個項都是一個dictht雜湊表,一般情況下,
        // 字典只使用ht[0]雜湊表,ht[1]雜湊表只對在對ht[0]進行rehash時使用
        dictht ht[2];
        // rehash的索引,當不在rehash時,為-1
        int rehashidx;
    } dict;
    
    typedef struct dictType {
        // 計算雜湊值的函式
        unsigned int (*hashFunction)(const void *key);
        // 複製鍵函式
        void *(*keyDup)(void *privdata, const void *key);
        // 複製值函式
        void *(*valDup)(void *privdata, const void *obj);
        // 對比鍵的函式
        int (*keyCompare)(void *privdata, const void *key1, const void *key2);
        // 銷燬鍵的函式
        void(*keyDestructor)(void *privdata, void *key);
        // 銷燬值的函式
        void(*valDestructor)(void *privdata, void *obj)
    } dictType;
    
  2. 雜湊演算法:當需要將一個新的鍵值對新增到字典中時,程式首先根據鍵值對中的鍵計算出雜湊值和索引值,然後再根據索引值,將包含新鍵值對的雜湊表節點放到雜湊表陣列的指定索引上。當字典被用作資料庫的底層實現時,或者雜湊鍵的底層實現時,Redis使用MurmurHash2演算法來結算鍵的雜湊,這種演算法的優點在於即使輸入的鍵是有規律的,演算法仍能給出一個很好的隨機性分佈,並且演算法的計算速度也非常快。

    // 計算雜湊值和索引值的方法如下:
    
    # 使用字典設定的雜湊函式,計算鍵key的雜湊值
    hash = dict -> type -> hashFunction(key);
    
    # 使用雜湊表的sizemask屬性和雜湊值計算出索引值,根據情況不同,ht[x]為ht[0]或ht[1]
    index = hash & dict -> ht[x].sizemask;
    

    如下圖,假設對圖4-3新增一個鍵值對k0、v0的節點,對k0雜湊的結果為8, 8 & 3 = 0,所以將k0、v0新增到雜湊表陣列中索引為0的位置上。

  3. 解決雜湊衝突:當有兩個或以上數量的鍵被分配到了雜湊表陣列的同一個索引上面時,我們稱這些鍵發生了雜湊衝突,雜湊表節點dictEntry中的next就是為了解決雜湊衝突,將產生衝突的鍵值對用next指標連線起來。同時,為了速度考慮,程式總是將新節點新增到連結串列的表頭位置(因為沒有指向表尾的指標),時間複雜度為O(1),排在其他已有節點的前面。

  4. rehash:隨著操作的不斷進行,雜湊表內儲存的鍵值對會逐漸的增多或減少,為了讓雜湊表的負載因子(used / size)維持在一個合理的範圍之內,需要對雜湊表的大小進行相應的擴充套件或者收縮。這些工作是通過rehash(重新雜湊)操作完成的,步驟如下:

    1. 為字典的ht[1]雜湊表分配空間,這個雜湊表的空間取決於要執行的操作,以及ht[0]當前包含的鍵值對數量(也就是ht[0].used屬性的值)

      1. 如果執行的是擴容操作,那麼ht[1]的大小為第一個大於等於ht[0].used*2的2^n(2的n次方冪);比如當前used為4, 4 * 2 = 8,而8是2的3次方,恰好是第一個大於等於4 * 2的2^n,所以將ht[1]雜湊表的大小設定成8,此時size=8,sizemask=7;
      2. 如果執行的是收縮操作,那麼ht[1]的大小為第一個大於等於ht[0].used的2^n。
    2. 將儲存在ht[0]中的所有鍵值對rehash到ht[1]上面:rehash指的是重新計算鍵的雜湊值和索引值,然後將鍵值對放到ht[1]雜湊表的指定位置上。

    3. 當ht[0]所含的所有鍵值對都遷移到了ht[1]之後,ht[0]變為空表,釋放ht[0],將ht[1]設定為ht[0],並在ht[1]新建立一個空白雜湊表,為下一次rehash做準備。

    4. 擴容或收縮觸發的條件:

      1. 伺服器目前沒有在執行BGSAVE命令或者BGREWRITEAOF命令,並且雜湊表的負載因子大於等於1,進行擴容;
      2. 伺服器目前正在執行BGSAVE命令或者BGREWRITEAOF命令,並且雜湊表的負載因子大於等於5,進行擴容;
      3. 當雜湊表的負載因子小於0.1時,程式自動開始對雜湊表執行收縮操作。
  5. 漸進式rehash:在上述所說將ht[0]中的鍵值對rehash到ht[1]中時,並不是一次性把所有的鍵值對全部rehash到ht[1]中的,而是分多次、漸進式地將ht[0]裡面的鍵值對慢慢地rehash到ht[1]中。具體步驟為:在rehash期間,程式除了執行指定的curd操作外,還會順帶將ht[0]雜湊表在rehashidx索引上的所有鍵值對rehash到ht[1],完成之後rehashidx+=1,當ht[0]中全部鍵值對移到ht[1]的時候,rehashidx置為-1。注意,在rehash期間,新加的鍵值對是到ht[1]中的,其他操作是先到ht[0]、沒有的話再到ht[1]中去操作

5、跳躍表

跳躍表(skiplist)是一種有序資料結構,它通過每個節點中維持多個指向其他節點的指標,從而達到快速訪問節點的目的。支援平均O(logN)、最壞O(N)複雜度的節點查詢,還可以通過順序性操作來批量處理節點。在大部分情況下,跳躍表的效率可以和平衡樹媲美,並且跳躍表的實現比平衡樹更為簡單。

Redis使用跳躍表作為有序集合鍵的底層實現之一,如果一個有序集合包含的元素數量比較多時,又或者有序集合中元素的成員是比較長的字串時,Redis就會使用跳躍表來作為有序集合鍵的底層實現。

  1. 跳躍表的實現:主要有redis.h/zskiplistNode和redis.h/zskiplist兩個結構定義,其中zskiplistNode結構用於表示跳躍表節點,而zskiplist結構則用於儲存跳躍表節點的相關資訊。
    1. 上圖中位於圖片最左邊的是zskiplist結構,主要包括以下屬性:
      1. header:指向跳躍表的表頭節點;
      2. tail:指向跳躍表的表尾節點;
      3. level:記錄目前跳躍表內,層數最大的那個節點的層次(表頭節點的層數不計算在內);
      4. length:記錄跳躍表的長度,也即是跳躍表目前包含節點的數量(不包含表頭節點)。
    2. 上圖中右面是四個zskiplistNode結構,主要包括以下屬性:
      1. 層(level):節點中的L1、L2、L3等字樣標記節點的各個層,分別表示第一層、第二層等以此類推。每個層都有兩個屬性:前進指標和跨度。前進指標用於訪問位於表尾方向的其他節點,而跨度則記錄了前進指標所指向節點和當前節點的距離。上圖中連線上帶有數字的箭頭就代表前進指標,數字就是跨度。跨度實際上是用來計算排位的,在查詢某個節點的過程中,將沿途訪問過的所有層的跨度累計起來,得到的結果就是目標節點在跳躍表中的排位。當程式從表頭向表尾進行遍歷時,訪問會沿著層的前進指標進行。
      2. 後退指標(backward):節點中用BW字樣標記節點的後退指標,它指向位於當前節點的前一個節點,後退指標在程式從表尾向表頭遍歷時使用。
      3. 分值(score):是一個double型別的浮點數,各個節點中的1.0、2.0、3.0是節點所儲存的分值,在跳躍表中,節點按各自所儲存的分值從小到大排列。
      4. 成員物件obj:各個節點中的o1、o2、o3是節點所儲存的成員物件。它是一個指標,指向一個字串物件,而字串物件則儲存著一個SDS值。

用於實現有序集合,另一個作用是在叢集節點中用作內部資料結構。

6、整數集合

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

  1. 整數集合的實現:整數集合是Redis用於儲存整數值的集合抽象資料結構,它可以儲存型別為int16_t、int32_t、int64_t的整數值,並且保證集合中不會出現重複元素。

    typedef struct intset {
        // 決定contents陣列中資料的型別,注意contents陣列中的資料型別並不是int8_t。
        uint32_t encoding;
        // 記錄contents陣列的長度
        uint32_t length;
        // contenst陣列是整數集合的底層實現,整數集合的每個元素都是都是contents陣列的一個數據項,
        // 各個項在陣列中按值的大小從小到大有序地排列,並且陣列中不包含任何重複項。
        int8_t contents[];
    }
    

    一個整數集合示例如下圖所示:

    其中,encoding值為INTSET_ENC_INT64,說明contens陣列中的資料型別為int64_t,由於陣列儲存了4個元素,所以contents陣列的大小為64 * 4 = 256位。

  2. 升級:每當我們要將一個新元素新增到整數集合裡面,並且新元素的型別比整數集合現有所有元素的型別都要長時,整數集合需要先進行升級,然後才能將新元素新增到整數集合裡面。步驟如下:

    1. 根據新元素的型別,擴充套件整數集合底層陣列的空間大小,併為新元素分配空間;
    2. 將底層陣列現有的所有元素都轉換成與新元素相同的型別,並將型別轉換後的元素放置到正確的位置上,而且在放置元素的過程中,需要繼續維持底層陣列的有序性質不變。
    3. 將新元素新增到底層陣列中。
    4. 因為引發升級的新元素的長度總是比整數集合現有所有元素的長度都大,所以這個新元素的值要麼大於所有現有元素,要麼小於所有現有元素。
    5. 由於每次新增元素都可能會引起升級,所以向整數集合中新增新元素的時間複雜度為O(N)
  3. 升級的好處:

    1. 提升靈活性:可以隨意的新增不同型別的整數到整數集合中;
    2. 節約記憶體:可以讓資料按需獲得空間,儘量節省記憶體。
  4. 降級:不支援!!!!!!

7、壓縮列表

壓縮列表(ziplist)是列表鍵和雜湊鍵的底層實現之一,當一個列表鍵只包含少量列表項,並且每個列表項要麼是小整數值,要麼就是長度比較短的字串,那麼Redis就會使用壓縮列表來做列表鍵的底層實現。

  1. 壓縮列表的構成:壓縮列表是Redis為了節約記憶體而開發的,是由一系列特殊編碼的連續記憶體塊組成的順序型資料結構。一個壓縮列表可以包含任意多個節點,每個節點可以儲存一個位元組陣列或者一個整數值。下圖為壓縮列表的各個組成部分:

  2. 壓縮列表節點的構成:每個壓縮列表節點都可以儲存一個位元組陣列或者一個整數值,都由previous_entry_length、encoding、content三個部分組成。

    1. previous_entry_length屬性以位元組為單位,記錄了壓縮列表中前一個節點的長度;如果前一節點的長度小於254位元組,那麼該屬性的長度為1位元組,儲存的便是前一個節點的長度;如果前一節點的長度大於等於254位元組,則該屬性的第一個位元組為0xFE,後四位元組儲存前一節點的長度。

    2. encoding屬性記錄了節點的content屬性所儲存資料的型別以及長度。值的最高位為00、01、10的是位元組陣列編碼,表示content屬性儲存著位元組陣列,後面的數表示content儲存內容的長度;值的最高位是11開頭的是整數編碼,表示content屬性儲存著整數值。

    3. content屬性負責儲存節點的值,可以是一個位元組陣列或整數,值的型別和長度由節點的encoding屬性決定。

  3. 連鎖更新:由於每個壓縮列表節點的previous_entry_length屬性都記錄了前一個節點的長度,而且該屬性的長度由前面節點的長度決定,所以假設在一個有多個連續的、長度介於250位元組到253位元組的節點e1-eN,如果在e1之前新增一個長度大於等於254位元組的新節點,將會使得e1-eN中的previous_entry_length長度擴充套件,這種現象為連鎖更新。

8、物件

以上主要是Redis用到的主要資料結構,包括簡單動態字串(SDS)、雙端連結串列、字典、壓縮列表、整數集合等等。Redis並沒有直接使用這些資料結構來實現鍵值對資料庫,而是基於這些資料結構建立了一個物件系統,包括字串物件、列表物件、雜湊物件、集合物件和有序集合物件這五種型別的物件,每種物件都用到了至少一種前面的資料結構。

除此,Redis的物件系統還實現了基於引用計數的記憶體回收機制,當程式不再使用某個物件時,這個物件所佔用的記憶體就會自動釋放掉;另外,Redis還通過引用計數實現了物件共享機制,可以在適當的條件下,通過讓多個數據庫鍵共享同一個物件來節約記憶體。

  1. 物件的型別和編碼:Redis中的每個物件都由一個redisObject結構表示,該結構中和儲存資料有關的三個屬性分別是type、encoding、和ptr屬性。

    1. 型別type:物件的type屬性記錄了物件的型別,這個屬性的值可以為下圖中的其中一個。對於Redis資料庫儲存的鍵值對來說,鍵總是一個字串物件,而值則可以是字串物件、列表物件、雜湊物件、集合物件、有序集合物件的一種。

      當對一個數據庫鍵執行TYPE命令時,命令返回的結果為資料庫鍵對應的值物件的型別,而不是鍵物件的型別,不同型別值物件的TYPE命令輸出如下:

    2. 編碼和底層實現:物件的ptr指標指向物件的底層實現資料結構,而這些資料結構由物件的encoding屬性決定。encoding記錄了物件所使用的編碼,也即是說這個物件使用了什麼資料結構作為物件的底層實現,對應關係如下圖:

      每種型別的物件都至少使用了兩種不同的編碼,如下圖:

      通過encoding屬性來設定物件所使用的編碼,而不是為特定型別的物件關聯一種固定的編碼,極大地提升Redis的靈活性和效率,因為Redis可以根據不同的使用場景來為一個物件設定不同的編碼,從而優化物件在某一場景下的效率。

  2. 字串物件:字串物件的編碼可以是int、raw或者embstr。

    1. 三種編碼方式的使用場景:
      1. 如果一個字串物件儲存的是整數值,並且這個整數值可以用long型別來表示,那麼字串物件會將整數值儲存在字串物件結構的ptr屬性裡面,並將字串物件的編碼設定為int;
      2. 如果字串物件儲存的是一個字串值,並且這個字串值的長度大於32位元組,那麼字串物件將使用一個簡單動態字串(SDS)來儲存這個字串值,並將其編碼設定為raw;
      3. 如果字串物件儲存的是一個字串值,並且這個字串值的長度小於等於32位元組,那麼字串物件將使用embstr編碼的方式來儲存這個字串值。
    2. 編碼的轉換:int和embstr編碼的字串物件在條件滿足的情況下,會被轉換為raw編碼的字串物件。
      1. 對於int編碼的字串來說,如果我們向物件執行了一些命令,使得這個物件儲存的不再是整數值,而是一個字串值,那麼字串物件的編碼將從int變為raw;
      2. 當對於embstr編碼的字串物件來執行任何修改命令時,程式會先將物件的編碼從embstr轉換成raw,然後再執行修改命令,因為在redis中,embstr編碼的字串物件是隻讀的。
  3. 列表物件:列表物件的編碼可以是ziplist或者linkedlist。

    1. 編碼介紹:
      1. ziplist編碼的列表物件使用壓縮列表作為底層實現,每個壓縮列表節點(entry)儲存了一個列表元素。假設執行RPUSH number 1 "there" 5,如果number使用的是ziplist編碼,這個值物件將會如下圖所示:
      2. linkedlist編碼的列表物件使用雙端連結串列作為底層實現,每個雙端連結串列節點(node)都儲存了一個字串物件,而每個字串物件都儲存了一個列表元素,儲存方式如下所示:
      3. 注意:linkedlist編碼的列表物件在底層的雙端連結串列結構中包含了多個字串物件,這種現象在雜湊物件、集合物件和有序集合物件中都會出現,也就是說,字串物件是五種物件中唯一會被其他四種類型物件巢狀的物件。
    2. 編碼轉換:
      1. 當列表物件可以同時滿足以下兩個條件時,列表物件使用ziplist編碼:列表物件儲存的所有字串元素的長度都小於64位元組;列表物件儲存的元素數量小於512個;不能同時滿足這兩個條件的列表物件需要使用linkedlist編碼。
      2. 對於使用ziplist編碼的列表物件來說,當兩個條件都不能滿足時,物件的編碼轉換操作就會執行,原本儲存在壓縮列表裡的所有列表元素都會被轉移並儲存到雙端連結串列裡面,物件的編碼也會從ziplist變為linkedlist。
  4. 雜湊物件:雜湊物件的編碼可以是ziplist或者hashtable。

    1. 編碼介紹:
      1. ziplist編碼的雜湊物件使用壓縮列表作為底層實現,每當有新的鍵值對要加入到雜湊物件時,程式會先將儲存了鍵的壓縮列表節點推入到壓縮列表表尾,然後再將儲存了值的壓縮列表節點推入到壓縮列表表尾,因此:儲存了同一鍵值對的兩個節點總是緊挨在一起,儲存鍵的節點在前,儲存值的節點在後;先新增到雜湊物件中的鍵值對會被放在壓縮列表的表頭方向,而後來新增到雜湊物件中的鍵值對會被放在壓縮列表的表尾方向。
      2. hashtable編碼的雜湊物件使用字典作為底層實現,雜湊物件中的每個鍵值對都使用一個字典鍵值對來儲存:字典的每個鍵都是一個字串物件,物件中儲存了鍵值對的鍵;字典的每個值都是一個字串物件,物件中儲存了鍵值對的值。
    2. 編碼轉換:
      1. 當雜湊物件可以同時滿足以下兩個條件時,雜湊物件使用ziplist編碼:雜湊物件儲存的所有鍵值對的鍵和值的字串長度都小於64位元組;雜湊物件儲存的鍵值對數量小於512個。不能同時滿足這兩個條件的雜湊物件需要使用hashtable編碼。
      2. 對於使用ziplist編碼的雜湊物件來說,當上述兩個條件中任意一個不能滿足時,物件的編碼轉換操作就會執行,原本儲存在壓縮列表裡的所有鍵值對都會被轉移並儲存到字典裡,物件的編碼也會從ziplist變為hashtable。
  5. 集合物件:集合物件的編碼可以是intset或者hashtable。

    1. 編碼介紹:

      1. intset編碼的集合物件使用整數集合作為底層實現,集合物件包含的所有元素都被儲存在整數集合裡面;
      2. hashtable編碼的集合使用字典作為底層實現,字典的每個鍵都是一個字串物件,每個字串物件包含了一個集合元素,而字典的值全部被設定為NULL。
    2. 編碼轉換:

      1. 當集合物件可以同時滿足以下兩個條件時,物件使用intset編碼:集合物件儲存的所有元素都是整數值;集合物件儲存的元素數量不超過512個。不能同時滿足這兩個條件的集合物件需要使用hashtable編碼。
      2. 對於使用intset編碼的集合物件來說,當上面兩個條件之一不滿足時,就會執行物件的編碼轉換操作,原本儲存在整數集合中的所有元素都會被轉移並儲存到字典裡面,物件的編碼也會從intset變為hashtable。
  6. 有序集合物件:有序集合獨享的編碼可以是ziplist或者skiplist。

    1. 編碼介紹:

      1. ziplist編碼的有序集合物件使用壓縮列表作為底層實現,每個集合元素使用兩個緊挨在一起的壓縮列表節點來儲存,第一個節點儲存元素的成員(member),而第二個元素則儲存元素的分值(score)。壓縮列表內的集合元素按分值從小到大進行排序,分值較小的元素被放置在靠近表頭的方向,而分值較大的元素則被放置在靠近表尾的方向。

      2. skiplist編碼的有序集合物件使用zset結構作為底層實現,一個zset結構同時包含一個字典和一個跳躍表:

      typedef struct zset {
         zskiplist *zsl;
         dict *dict
      } zset;
      

      zset結構中的zsl跳躍表按分值從小到大儲存了所有集合元素,每個跳躍表節點都儲存了一個集合元素:跳躍表節點的object屬性儲存了元素的成員,而跳躍表節點的score則儲存了元素的分值,通過跳躍表,程式可以對有序集合進行範圍型操作;除此,zset結構中的dict字典為有序集合建立了一個從成員到分值的對映,字典中的每個鍵值對都儲存了一個集合物件:字典的鍵儲存了元素的成員,而字典的值則儲存了元素的分支。通過字典,可以用O(1)複雜度查詢給定成員的分值。

      1. 有序集合每個元素的成員都是一個字串物件,而每個元素的分值都是一個double型別的浮點數。雖然zset結構同時使用跳躍表和字典來儲存有序集合元素,但是這兩種資料結構都會通過指標來共享相同的成員和分值,所以同時使用跳躍表和字典來儲存集合元素不會產生任何重複成員或者分值,也不會因此浪費額外的記憶體。

    2. 編碼轉換:

      1. 當有序集合物件可以同時滿足以下兩個條件時,使用ziplist編碼:有序集合儲存的元素數量小於128個;有序集合儲存的所有元素成員的長度都小於64位元組。不能同時滿足上述條件時,將使用skiplist編碼。
      2. 對於使用ziplist編碼的有序集合物件,當上述兩個條件任一不滿足時,就會執行物件的編碼轉換操作。
  7. 型別檢查與命令多型

    1. 一種命令只能對特定型別的鍵執行:
      1. SET、GET、APPEND、STRLEN等命令只能對字串鍵執行;
      2. HSET、HGET、HDEL、HLEN等命令只能對雜湊鍵執行;
      3. RPUSH、LPOP、LINSERT、LLEN等命令只能對列表鍵執行;
      4. SADD、SPOP、SINTER、SCARD等命令只能對集合鍵執行;
      5. ZADD、ZSCARD、ZRANK、ZSCORE等命令只能對有序集合鍵執行。
    2. 型別檢查:在執行一個特定型別的命令之前,Redis會先檢查輸入鍵的型別與資料庫已存在鍵的型別是否匹配(通過資料庫儲存物件的type屬性),然後再決定是否執行給定的命令。
    3. 多型命令的實現:如果對一個鍵執行LLEN命令,那麼伺服器除了要確保執行命令的是列表鍵外,還需要根據鍵的值物件所使用的編碼來選擇正確的LLEN命令實現:
      1. 如果列表物件的編碼為ziplist,那麼說明列表的實現為壓縮列表,程式將使用ziplistLen函式來返回列表的長度;
      2. 如果列表物件的編碼為linkedList,那麼說明列表物件的實現為雙端連結串列,程式將使用ListLength函式來返回雙端連結串列的長度。
  8. 記憶體回收

由於C語言並不具備自動記憶體回收功能,所以Redis在自己的物件系統中構建了一個引用計數實現的記憶體回收機制,通過這個機制,程式可以通過跟蹤物件的引用計數資訊,在適當的時候自動釋放物件並進行記憶體回收

typedef struct redisObject {
   // ...

   // 引用計數
   int refCount;
} redisObject;
  1. 在建立一個新物件時,引用計數的值會被初始化為1;

  2. 當物件被一個新程式使用時,它的引用計數會被+1;

  3. 當物件不再被一個程式使用時,它的引用計數會被-1;

  4. 當物件的引用計數變為0時,物件所佔用的記憶體會被釋放

  5. 物件共享:除了用於實現引用計數記憶體回收機制之外,物件的引用計數屬性還帶有物件共享的作用。

    1. 在Redis中,讓多個鍵共享同一個值物件需要的步驟:將資料庫鍵的值指標指向一個現有的值物件;將共享的值物件的引用計數加一;
    2. 共享物件機制對於節約記憶體非常有幫助,資料庫中儲存的相同值物件越多,物件共享機制就能節約越多的記憶體;
    3. 需要注意的是,Redis只對包含整數值的字串物件進行共享,因為當伺服器考慮將一個共享物件設定為鍵的值物件時,程式會先檢查給定的共享物件和鍵想建立的目標物件是否完全相同,只有在完全相同的情況下,才能共享物件。如果共享物件是儲存的整數值的話,驗證的時間複雜度為O(1);如果是字串值的話,時間複雜度為O(N);如果為多個值的話,時間複雜度為O(N^2);所以出於CPU效能的考慮,只儲存整數值。
  6. 物件的空轉時長:redisObject結構體中還包含一個lru屬性,該屬性記錄了物件最後一次被命令程式訪問的時間,如果伺服器被打開了maxmemory選項,並且伺服器用於回收記憶體的演算法為volatile-lru或者allkeys-lru,那麼當伺服器佔用的記憶體數超過了maxmemory選項所設定的上限值時,空轉時長較高的那部分鍵會優先被伺服器釋放,從而回收記憶體。