Redis數據結構及對象(上)

Redis對象及底層數據結構


  redis一共有五大經常使用的對象,用type命令便可查看當前鍵對應的對象類型,分別是string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),可是這些只是對外的數據結構,實際上每個對象都有兩到三種不一樣底層數據結構實現,能夠經過object encoding命令查看鍵值對應的底層數據結構實現,redis

下表即爲每種對象所對應的底層數據結構實現。算法

類型 編碼 底層數據結構
string int 整數值
string raw 簡單動態字符串
string embstr 用embstr編碼的簡單動態字符串
hash ziplist 壓縮列表
hash hashtable 字典
list ziplist 壓縮列表
list linkedlist 雙端列表
set intset 整數集合
set hashtable 字典
zset ziplist 壓縮列表
zset skiplist 跳錶和字典

簡單動態字符串(SDS)


定義

  redis並無使用C字符串,而是使用了名爲簡單動態字符串(SDS)的結構,SDS的定義以下:數組

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

    // 記錄 buf 數組中未使用字節的數量
    int free;

    // 字節數組,用於保存字符串
    char buf[];
};
複製代碼
  • len:記錄字符串長度,大小爲4個字節
  • free: 記錄buf[]中未被使用字節數量,大小爲4個字節
  • buf[]: 保存字符串,大小爲字符串大小+1,由於buf[]最後一個字節保存'\0' 因此sds的總大小爲 = 4 + 4 + size(str) + 1
    sds圖示

SDS的做用

  那麼redis爲何要使用看起來更佔空間的SDS結構呢?主要有如下幾個緣由:安全

  1. O(1)複雜度得到string的長度  相比於C字符串須要遍歷string才能得到長度(複雜度O(N)),SDS直接查詢len的數值便可。
  2. 防止緩衝區溢出  當修改C字符串時,若是沒有分配夠足夠的內存,很容易形成緩衝區溢出。而使用SDS結構,當修改字符串時,會自動檢測當前內存是否足夠,若是內存不夠,則會擴展SDS的空間,從而避免了緩衝區溢出。
  3. 減小修改字符串帶來的頻繁的內存分配  每次增加或縮短C字符串,都須要從新分配內存,而redis常常被用在數據修改頻繁的場合,因此SDS採用了兩種策略從而避免了頻繁的內存分配。  ①空間預分配   如上文所述,SDS會自動分配內存,若是修改後字符串內存佔用小於1MB,則會分配一樣大小的未使用內存空間。(eg len: 20kb free: 10kb→ len: 40kb free 40kb),若是大於1MB,則分配1MB未使用內存空間。如此一來就能夠避免由於字符串增加帶來的頻繁空間分配。  ②惰性刪除   當縮短字符串時,SDS並無釋放掉相應的內存,而是保留下來,用free記錄未使用的空間,爲之後的增加字符串作準備。
  4. 二進制安全  SDS會以處理二進制數據的形式存取buf中的內容,從而讓SDS不只能夠保存任意編碼的文本信息,還能夠保存諸如圖片、視頻、壓縮文件等二進制數據。

雙端列表


定義

  雙端列表做爲一種經常使用的數據結構,當一個list的長度超過512時,那麼redis將使用雙端列表做爲底層數據結構。下面是一個列表節點的定義:bash

typedef struct listNode {

    // 前置節點
    struct listNode *prev;

    // 後置節點
    struct listNode *next;

    // 節點的值
    void *value;

} listNode;
複製代碼

  多個列表節點串聯起來即可實現雙端列表。數據結構

typedef struct list {

    // 表頭節點
    listNode *head;

    // 表尾節點
    listNode *tail;

    // 鏈表所包含的節點數量
    unsigned long len;

    // 節點值複製函數
    void *(*dup)(void *ptr);

    // 節點值釋放函數
    void (*free)(void *ptr);

    // 節點值對比函數
    int (*match)(void *ptr, void *key);

} list;
複製代碼

  能夠看到雙端列表是一個無環雙端帶表頭表尾節點的鏈表。函數

字典


定義

散列表Hash table,也叫哈希表),是根據鍵而直接訪問在內存存儲位置的數據結構。也就是說,它經過計算一個關於鍵值的函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數稱作散列函數,存放記錄的數組稱作散列表性能

  當hashtable的類型沒法知足ziplist的條件時(元素類型小於512且全部值都小於64字節時),redis會使用字典做爲hashtable的底層數據結構實現。redis的字典(dict)中維護了兩個哈希表(table),而每一個哈希表包含了多個哈希表節點(entry)。下面分別來介紹這三個對象。優化

哈希表節點

typedef struct dictEntry {

    // 鍵
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下個哈希表節點,造成鏈表
    struct dictEntry *next;

} dictEntry;
複製代碼
  • key:鍵值對中的鍵。
  • v: 鍵值對中的值,能夠看到值能夠爲一個指針,或者是一個uint64整數或者int64整數。
  • next:是爲了用鏈地址法解決hash衝突。

哈希表

typedef struct dictht {

   // 哈希表數組
   dictEntry **table;

   // 哈希表大小
   unsigned long size;

   // 哈希表大小掩碼,用於計算索引值
   // 老是等於 size - 1
   unsigned long sizemask;

   // 該哈希表已有節點的數量
   unsigned long used;

} dictht;
複製代碼
  • table:是一個保存着指向全部節點指針的數組。
  • size: 記錄了table數組的大小。
  • sizemask: 用於和hash值一塊兒計算索引值(index = hash & sizemask )

字典

typedef struct dict {

   // 類型特定函數
   dictType *type;

   // 私有數據
   void *privdata;

   // 哈希表
   dictht ht[2];

   // rehash 索引
   // 當 rehash 不在進行時,值爲 -1
   int rehashidx; /* rehashing not in progress if rehashidx == -1 */

} dict;
複製代碼
  • type 屬性和 privdata 屬性是針對不一樣類型的鍵值對, 爲建立多態字典而設置的。
  • 字典內部有兩個哈希表,這樣作的目的是爲rehash作準備。
    字典圖示

hash算法

  當在哈希表中存取數據時,首先須要用hash算法算出鍵值對中的鍵所對應的hash值,而後再根據根據table數組的大小取模,計算出對應的索引值,再繼續接下來的操做。redis使用了MurmurHash2 算法來計算鍵的哈希值,又使用了快速冪取模算法下降了取模的複雜度。整個過程以下:ui

hash = dict->type->hashFunction(k0);
index = hash & dict->ht[0].sizemask;
複製代碼

  當hash衝突發生時則採用鏈地址法解決hash衝突。

rehash

  當哈希表保存的鍵值對愈來愈多時,哈希表的負載因子(load factor = used / size)愈來愈大, 本來O(1)複雜度的查找也會漸漸趨向於O(N),爲了保證哈希表的負載因子在必定的範圍以內。redis須要動態的調整table數組的大小,其中最重要的即是rehash過程。rehash分如下的幾個步驟:

  1. 爲字典的 ht[1] 哈希表分配空間,須要注意的是新的size必須是2^n,這主要是爲了配合快速冪取模算法。
  2. 將ht[0]上的鍵值對rehash到ht[1]上,即從新計算ht[0]上全部鍵值對的hash值和索引值,而後分配到ht[1]上,當原來的哈希表數據量很大時可能會引發線程的阻塞,因此redis採用漸進式的rehash方式。
  3. ht[0]表釋放,原子性的替換ht[1]至ht[0],並建立一個空的哈希表分配至ht[1]

漸進式rehash

  redis的rehash過程並非一次性集中rehash,而是分批間隔式的,在dict中的rehashidx即是爲此服務。   相較於一次性的rehash,漸進式的rehash多了下面這些步驟:

  1. 開始rehash時,將rehashidx置爲0。
  2. 當完成了一次rehash後,將rehashidx自增1,直到遍歷完全部的table數組。
  3. 在rehash過程當中,若是有對字典進行增長,則只增長ht[1],若是是查找,則先查找ht[0],若是找不到則去查找ht[1],而若是是刪除和更新,則ht[0]和ht[1]同步操做。
  4. 完成全部rehash後,將rehashidx置爲-1。

  這是比較典型的分而治之的思想,將一次性集中做業分散,下降了系統的風險。

跳躍表


定義

  跳錶的的查找複雜度爲平均O(logN)/最壞O(N)。在不少場合下做爲替代平衡樹的數據結構,在redis中,若是有序集合的屬性不知足ziplist的要求,則將跳錶做爲有序集合的底層實現。

跳錶圖示
  上圖即爲一個完整的跳錶,其中有幾點比較重要,這個跳錶一共有三個節點再加上一個頭節點,最高有五層。一個跳躍表包含了兩種對象,一個是跳躍表節點,一個是跳躍表。

跳躍表節點

typedef struct zskiplistNode {
    // 後退指針
    struct zskiplistNode *backward;

    // 分值
    double score;

    // 成員對象
    robj *obj;

    // 層
    struct zskiplistLevel {

        // 前進指針
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;
複製代碼
  • backward:後退指針,和雙端列表同樣,指向上一個節點。
  • score:分值,有序列表的排序依據。
  • obj:成員對象,實際上爲一個SDS,在有序集合中分值能夠重複,但成員對象不能重複。
  • level:層,跳錶的關鍵所在,在條表中每一層包含了1到n個節點,在有序的狀況下,能夠快速遍歷數組。
  • forward:下一個節點的對象,這裏的下一個表明是第一個或者是第n個。
  • span: 下一個節點和如今節點的距離。

跳躍表

typedef struct zskiplist {

    // 表頭節點和表尾節點
    struct zskiplistNode *header, *tail;

    // 表中節點的數量
    unsigned long length;

    // 表中層數最大的節點的層數
    int level;

} zskiplist;
複製代碼

跳躍表中保存了頭尾節點,方便遍歷,還保存了節點的數量,能夠在O(1) 複雜度內返回跳躍表的長度。

整數集合


定義

  當集合的值全爲整數且集合的長度不超過512時,redis採用整數集合做爲集合的底層數據結構。

typedef struct intset {

    // 編碼方式
    uint32_t encoding;

    // 集合包含的元素數量
    uint32_t length;

    // 保存元素的數組
    int8_t contents[];

} intset;
複製代碼
  • encoding:整數集合中元素的編碼方式

INTSET_ENC_INT16 , contents 就是一個 int16_t 類型的數組(最小值爲 -32,768 ,最大值爲 32,767 )。 INTSET_ENC_INT32 , contents 就是一個 int32_t 類型的數組(最小值爲 -2,147,483,648 ,最大值爲 2,147,483,647 )。 INTSET_ENC_INT64 , contents 就是一個 int64_t 類型的數組(最小值爲 -9,223,372,036,854,775,808 ,最大值爲 9,223,372,036,854,775,807 )。

  • length:數量
  • contents:集合元素 雖然contents看起來是int8_t,可是它的具體內容的存取仍是按encoding的方式完成。

升級

  redis採用多種編碼的方式,主要仍是爲了省內存。當集合中加入了不符合當前集合編碼的數字時,數組集合會自動更新至能匹配到的編碼,值得注意的是,這種升級是不可逆的,只能由小往大,不能降級。如此一來,就可以在存放小數據時,剩下很大的空間,並且也沒必要爲編碼不匹配的事情而煩惱了。

壓縮列表


  壓縮列表是redis又一個爲了節省內存所作的優化,是list/hash/zset的底層數據結構之一,當數據值不大且數量較低時,redis都會使用壓縮列表。

壓縮列表圖示

  • zlbytes:記錄整個壓縮列表佔用的內存字節數:在對壓縮列表進行內存重分配, 或者計算 zlend 的位置時使用。
  • zltail:記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少字節: 經過這個偏移量,程序無須遍歷整個壓縮列表就能夠肯定表尾節點的地址。
  • zllen:記錄了壓縮列表包含的節點數量: 當這個屬性的值小於 UINT16_MAX (65535)時, 這個屬性的值就是壓縮列表包含節點的數量; 當這個值等於 UINT16_MAX 時, 節點的真實數量須要遍歷整個壓縮列表才能計算得出。
  • entryX:壓縮列表包含的各個節點,節點的長度由節點保存的內容決定。
  • zlend:特殊值 0xFF (十進制 255 ),用於標記。壓縮列表的末端。

  壓縮列表和雙端列表有些相似,不過一個用指針銜接起來,一個則是用數組和長度銜接起來。下面來看一看壓縮列表節點的定義:

節點圖示

  • prevrawlen:前置節點的長度,至關於雙端列表中的前置指針,經過它能夠計算出前置節點的地址。
  • coding: 和正數集合相似,是爲了代表content中是何種數據
  • content: 數據

總結


  本文對於redis常見的數據結構及其底層實現進行了分析和梳理,但願可以理清這些底層數據結構對於redis高性能的做用和影響。

相關文章
相關標籤/搜索