redis底層數據結構

一、redis中數據對象

redis有五大數據類型, 通過統一對象redisObject存儲, redisObject的結構主要包含以下部分:

  • type屬性存儲對象的類型, 也就是string, list, hash, set, zset中的一種。可以通過type命令查看。
  • encoding屬性記錄對象的所使用的編碼,即底層數據結構。
  • ptr 指向底層數據結構的指針。
1 typedef struct redisObject {
2     // 類型
3     unsigned type:4;
4     // 編碼
5     unsigned encoding:4;
6     // 指向底層實現數據結構的指針
7     void *ptr;
8     // ...
9 } robj;

redis為每種數據類型,提供了兩種以上的底層數據結構實現,可以通過type和object encoding這兩個命令查看數據對象的類型和數據結構。

127.0.0.1:6379> set key1 100
OK
127.0.0.1:6379> type key1
string
127.0.0.1:6379> object encoding key1
"int"
127.0.0.1:6379> set key2 'abc'
OK
127.0.0.1:6379> type key2
string
127.0.0.1:6379> object encoding key2
"embstr"

二、不同數據類型對應的底層數據結構

  1. 字符串
  • int:8個字節(jié)的長整型。
  • embstr:小于等于39個字節(jié)的字符串。
  • raw:大于39個字節(jié)的字符串。
    Redis會根據當前值的類型和長度決定使用哪種內部編碼實現。
  1. 哈希
  • ziplist(壓縮列表):當哈希類型元素個數小于hash-max-ziplist-entries 配置(默認512個)、同時所有值都小于hash-max-ziplist-value配置(默認64 字節(jié))時,Redis會
    使用ziplist作為哈希的內部實現,ziplist使用更加緊湊的 結構實現多個元素的連續(xù)存儲,所以在節(jié)省內存方面比hashtable更加優(yōu)秀。
  • hashtable(哈希表):當哈希類型無法滿足ziplist的條件時,Redis會使 用hashtable作為哈希的內部實現,因為此時ziplist的讀寫效率會下降,
    而 hashtable的讀寫時間復雜度為O(1)。
  1. 列表
  • ziplist(壓縮列表):當列表的元素個數小于list-max-ziplist-entries配置 (默認512個),同時列表中每個元素的值都小于list-max-ziplist-value配置時 (默認64字節(jié)),
    Redis會選用ziplist來作為列表的內部實現來減少內存的使 用。
  • linkedlist(鏈表):當列表類型無法滿足ziplist的條件時,Redis會使用 linkedlist作為列表的內部實現。
  • quicklist ziplist和linkedlist的結合, 以ziplist為節(jié)點的鏈表(linkedlist)
  1. 集合
  • intset(整數集合):當集合中的元素都是整數且元素個數小于set-max- intset-entries配置(默認512個)時,Redis會選用intset來作為集合的內部實 現,從而減少內存的使用。
  • hashtable(哈希表):當集合類型無法滿足intset的條件時,Redis會使 用hashtable作為集合的內部實現。
  1. 有序集合
  • ziplist(壓縮列表):當有序集合的元素個數小于zset-max-ziplist- entries配置(默認128個),同時每個元素的值都小于zset-max-ziplist-value配 置(默認64字節(jié))時,
    Redis會用ziplist來作為有序集合的內部實現,ziplist 可以有效減少內存的使用。
  • skiplist(跳躍表):當ziplist條件不滿足時,有序集合會使用skiplist作 為內部實現,因為此時ziplist的讀寫效率會下降。
三、數據結構實現

1. int

int保存的是long類型的整數,8個字節(jié)長整型,直接保存在redisObject對象的ptr屬性中。
2. embstr
embstr保存小于等于39個字節(jié)的字符串,使用動態(tài)字符串(SDS)實現。直接保存在redisObject對象的ptr屬性中,只需要分配一次內存, 即創(chuàng)建redisObject對象。

3. raw
raw保存大于39個字節(jié)的字符串,使用動態(tài)字符串(SDS)實現,需要單獨分配內存給sdshdr(SDS)結構。

注意: 在redis 3.2之后,embstr和raw改為以44字節(jié)為分界線

SDS
redis沒有采用c的字符串結構,而是構建了動態(tài)字符串的數據結構,先來看看結構源碼:

struct sdshdr{
     //記錄buf數組中已使用字節(jié)的數量
     //等于 SDS 保存字符串的長度
     int len;
     //記錄 buf 數組中未使用字節(jié)的數量
     int free;
     //字節(jié)數組,用于保存字符串,最后一個位置存儲的是空字符'\0', 不計入len
     char buf[];
}

SDS的優(yōu)化:

  • 空間預分配,不用擔心字符串變更造成的內存溢出。
    • 如果空間夠用,則不會額外分配空間, 通過free屬性。
    • 如果修改后的 SDS 長度 len 小于 1MB,那么程序分配和 len 屬性相等的未使用空間,此時 free 和 len 的值相同。所以此時數組的實際長度為 free + len + 1byte(額外的空字符 1 個字節(jié))。
    • 如果修改后的 SDS 長度大于 1MB,那么程序分配 1MB 的未使用空間。實際長度為 len + 1MB + 1byte。
  • 惰性空間釋放,字符串縮短,并不是立即重新分配內存,釋放空間。
  • 常數時間復雜度讀取字符串長度, 通過len屬性
  • 二進制安全, c語言中空字符意味著字符串結束,SDS則不需要考慮,通過len判斷是否結束。

分界線39或44的來源

  • Redis中內存分配使用的是 jemalloc,jemalloc 分配內存的時候是按照 8、16、32、64 作為 chunk 的單位進行分配的。
  • 為了保證采用embstr編碼方式的字符串能被 jemalloc 分配在同一個 chunk 中,整個redisObject不超過64
  • 因此OBJ_ENCODING_EMBSTR_SIZE_LIMIT = 64 - 16(redisObject) - 4(sdshdr的len屬性) - 4(sdshdr的free屬性) - 1(sdshdr的buf最后一位'\0') = 39。
  • len和size之前使用unsign int,占4個字節(jié),調整為調整為unit8_t, 占1個字節(jié)。并追加了一個char flag 1個字節(jié)。

4. ziplist
壓縮列表是 Redis 為了節(jié)約內存而實現的,是一系列特殊編碼的連續(xù)內存塊組成的順序型數據結構。 結構如下圖:

  • zlbytes 4字節(jié),記錄整個壓縮列表占用的內存字節(jié)數。
  • zltail 4字節(jié),記錄壓縮列表表尾節(jié)點的位置。
  • zllen 2字節(jié),記錄壓縮列表節(jié)點個數。超過需要65535遍歷。
  • entry 列表節(jié)點,長度不定,由內容決定。
  • zlend 1字節(jié),0xFF 標記壓縮的結束。
    壓縮列表的節(jié)點結構如下圖:


  • privious_entry_length占四個字節(jié),取決于前一個節(jié)點的長度,小于254字節(jié),就是占1個字節(jié),否則就是5個字節(jié)。
  • enncoding 記錄節(jié)點的content保存數據的類型和長度。

壓縮列表的遍歷:
通過指向表尾節(jié)點的位置指針p1, 減去節(jié)點的previous_entry_length,得到前一個節(jié)點起始地址的指針。如此循環(huán),從表尾遍歷到表頭節(jié)點。

5. linkedlist
雙向鏈表結構,表頭節(jié)點的前置節(jié)點和表尾節(jié)點的后置節(jié)點都是NULL,是無環(huán)鏈表。鏈表結構源碼如下:

typedef struct list{
    //  表頭節(jié)點
    listNode *head;
    //  表尾節(jié)點
    listNode *tail;
     //  鏈表節(jié)點數
    unsigned long len;
    // 節(jié)點值復制函數
    void (*free) (void *ptr);
    // 節(jié)點值釋放函數
    void (*free) (void *ptr);
    //  節(jié)點值對比函數
    int (*match) (void *ptr,void *key);
}list;

6. quicklist
一個由ziplist組成的雙向鏈表(ziplist和linklist結合),即鏈表的每個節(jié)點都是ziplist;redis3.2新增的數據結構,用于列表的實現。quicklist結構源碼:

typedef struct quicklist {
    // 指向頭部quicklist節(jié)點的指針
    quicklistNode *head;
    //  指向尾部quicklist節(jié)點的指針
    quicklistNode *tail;
    // quicklist中所有ziplist中的entry節(jié)點數量。
    unsigned long count;
    // quicklist的鏈表節(jié)點數量
    unsigned int len; 
    // 保存ziplist的大小,配置文件設定,占16bits
    int fill : 16;        
    // 保存壓縮程度值,配置文件設定,占16bits,0表示不壓縮
    unsigned int compress : 16; 
} quicklist;

7. intset
當一個集合中只有整數元素,就會使用intset結構。結構源碼:

typedef struct intset {
    // 編碼方式
    uint32_t encoding;
    // 集合包含的元素數量
    uint32_t length;
    // 保存元素的數組
    int8_t contents[];
} intset;
  • encoding有三種屬性值:INTSET_ENC_INT16,INTSET_ENC_INT32, INTSET_ENC_INT64, 分別表示用int16_t, int32_t, int64_tL類型的數組,進行數據存儲。
    當元素大小超過時,會進行升級。(不支持降級)

8. hashtable
redis的hash表,采用鏈地址法解決沖突。
哈希表結構:

typedef struct dict {
    // 類型特定函數
    dictType *type;
    // 私有數據
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 當 rehash 不在進行時,值為 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;

其中dictht ht[0]為正常情況下使用,ht[1]為rehash過程使用。

typedef struct dictht {
    // 哈希表數組
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩碼,用于計算索引值, 總是等于 size - 1
    unsigned long sizemask;
    // 該哈希表已有節(jié)點的數量
    unsigned long used;
} dictht;

hash節(jié)點結構:

typedef struct dictEntry {
    // 鍵
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下個哈希表節(jié)點,形成鏈表
    struct dictEntry *next;  // 單鏈表結構
} dictEntry;
  • redis的hash節(jié)點采用單鏈表結構,哈希表沒有使用紅黑樹,對哈希節(jié)點進行優(yōu)化。
    ps: 在hash沖突嚴重時,大量hashcode落到同一個hash節(jié)點上,此時hash表退化成單鏈表。
    在JDK1.8中采用HashTable對進行優(yōu)化,當鏈表數量超過8時,使用紅黑樹替代鏈表。

  • rehash 由負載因子決定什么時候進行rehash。過程如下:

  1. 為ht[1]分配空間,大小為比當前ht[0]已使用值的兩倍大的第一個2的整數冪。如,已使用空間7,則分配比7*2大的最近的2的整數冪,即16。
  2. 將ht[0]中所有鍵值對,rehash到ht[1]上。
  3. 完成遷移后,釋放ht[0], 將ht[1]設置為ht[0], 在ht[1]處新建空白哈希表,為下一次rehash做準備。
  • 漸進式rehash 當鍵值對數量巨大,一次性全部rehash將造成阻塞,服務暫停。所以拆分成多次,慢慢的將ht[0]中的數據rehash到ht[1]中。過程如下:
  1. 為ht[1]分配空間,同時持有兩個哈希表(一個空表、一個有數據)。
  2. 維持計數器rehashidx,初始值為0,表示rehash開始。
  3. 每次增刪改查,都順帶將ht[0]中的數據遷移到ht[1], rehashidx++.
  4. 直到rehash完成,rehashidx設置為-1。
    漸進式hash中,更新、刪除、查找都會在兩個hash表上進行。新增操作只在ht[1]上進行,保證ht[0]只減不增,直到成為空表。
    (假如ht[0]有冷門數據一直不被操作,ht[0]一直沒有清空,ht[1]觸發(fā)新的rehash閾值怎么辦?)

9. skiplist跳躍表


跳躍表的結構如下:

typedef struct zskiplist {
    // 表頭節(jié)點和表尾節(jié)點
    struct zskiplistNode *header, *tail;
    // 跳表節(jié)點的數量
    unsigned long length;
    // 跳表層數
    int level;
} zskiplist;

節(jié)點結構如下:

typedef struct zskiplistNode {
    // 后退指針
    struct zskiplistNode *backward;
    // 分數值
    double score;
    // 成員對象
    robj *obj;
    // 層
    struct zskiplistLevel {
        // 前進指針
        struct zskiplistNode *forward;
        // 跨度,記錄兩個節(jié)點間的距離
        unsigned int span;
    } level[];
} zskiplistNode;
  • skiplist是一種以空間換取時間的結構。由于鏈表,無法進行二分查找,因此借鑒數據庫索引的思想,提取出鏈表中關鍵節(jié)點(索引),先在關鍵節(jié)點上查找,再進入下層鏈表查找。提取多層關鍵節(jié)點,就形成了跳躍表。
  • 跳躍表在es的lucene索引也有有應用。

為什么用跳躍表不用平衡樹

  1. skiplist算法實現簡單得多。
  2. 跳躍表和平衡樹的插入刪除時間復雜度都是O(logn),不過平衡樹的插入和刪除操作引發(fā)樹結構的調整,操作復雜。skiplist只需要修改相鄰節(jié)點的指針,操作簡單。
  3. 查詢單個key,跳躍表和平衡樹的時間復雜度都是O(logn),大體相當。
  4. 范圍查找,平衡樹要復雜,skiplist適合zset各種操作。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377