Redis:底層數據結構分析

[TOC]html

引用:Redis詳解(四)------ redis的底層數據結構redis

判斷值類型: object encoding keyname算法

127.0.0.1:6379> set k1  str
OK
127.0.0.1:6379> set k2 123
OK
127.0.0.1:6379> Object encoding  k1
"embstr"
127.0.0.1:6379> Object encoding  k2
"int"
127.0.0.1:6379> lpush list1 1 2 3
(integer) 3
127.0.0.1:6379> Object encoding list1
"quicklist"
127.0.0.1:6379> hset testhash key1 009
(integer) 1
127.0.0.1:6379> object encoding testhash
"ziplist"
127.0.0.1:6379> hset testhash key2 00ososkskkalalkskskaaakasmasd,jansm,namsnasnda,msn,akdwj,kjallllllaaaaassjjjjjjjjacacascascascmnascaksc,ascmascna,klna,cksa,cksans,can,scamcs9
(integer) 1
127.0.0.1:6379> object encoding testhash
"hashtable"
127.0.0.1:6379>
複製代碼

數據結構數據庫

數據結構 定義常量
整數類型 REDIS_ENCODING_INT "int"
embstr字符串類型 REDIS_ENCODING_EMBSTR "embstr"
簡單動態字符串 REDIS_ENCODING_RAW "raw"
字典類型 REDIS_ENCODING_HT "hashtable"
雙端鏈表 REDIS_ENCODING_LINKEDLIST "linkedlist"
壓縮列表 REDIS_ENCODING_ZIPLIST "ziplist"
整數集合 REDIS_ENCODING_INTSET "intset"
跳錶和字典 REDIS_ENCODING_SKIPLIST "skiplist"

對應關係數組

1. string  ==> raw|embstr|int
2. list ==> quicklist
3. hash ==> ziplist|hashtable
4. set ==> intset|hashtable
5. zset ==> ziplist|skiplist
6. stream => stream
複製代碼

1.SDS(simple dynamic string)簡單動態字符串

  • 結構定義
struct sdshdr{
     //記錄buf數組中已使用字節的數量
     //等於 SDS 保存字符串的長度
     int len;
     //記錄 buf 數組中未使用字節的數量
     int free;
     //字節數組,用於保存字符串
     char buf[];
}
複製代碼

SDS保存的字符串結構圖示: 安全

image.png

優點
  1. 常數複雜度獲取字符串長度bash

    • 因爲 len 屬性的存在,咱們獲取 SDS 字符串的長度只須要讀取 len 屬性,時間複雜度爲 O(1)。而對於 C 語言,獲取字符串的長度一般是通過遍歷計數來實現的,時間複雜度爲 O(n)。經過 strlen key 命令能夠獲取 key 的字符串長度。
  2. 杜絕緩衝區溢出數據結構

    • 在 C 語言中使用 strcat 函數來進行兩個字符串的拼接,一旦沒有分配足夠長度的內存空間,就會形成緩衝區溢出。而對於 SDS 數據類型,在進行字符修改的時候,會首先根據記錄的 len 屬性檢查內存空間是否知足需求,若是不知足,會進行相應的空間擴展,而後在進行修改操做,因此不會出現緩衝區溢出。
  3. 減小修改字符串的內存從新分配次數函數

  4. 二進制安全ui

  5. 兼容部分C字符串函數

    C字符串與Redis簡單動態字符串對比圖.png
    SDS 除了保存數據庫中的字符串值之外,SDS 還能夠做爲緩衝區(buffer):包括 AOF 模塊中的AOF緩衝區以及客戶端狀態中的輸入緩衝區

2.鏈表

  • 鏈表的定義

//鏈表節點
typedef  struct listNode{
       //前置節點
       struct listNode *prev;
       //後置節點
       struct listNode *next;
       //節點的值
       void *value;  
}listNode
複製代碼

  經過多個 listNode 結構就能夠組成鏈表,這是一個雙端鏈表,Redis還提供了操做鏈表的數據結構:

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

鏈表結構圖示.png

  • 鏈表特性

    • 雙端:鏈表具備前置節點和後置節點的引用,獲取這兩個節點的時間複雜度都爲O(1)
    • 無環:表頭節點的prev指針和表節點的next指針向NULL
    • 長度計數器:獲取長度時間複雜度O(1)
    • 多態:鏈表節點使用void*指針來保存節點值,能夠保存不一樣類型的值

3.字典

字典又稱爲符號表或者關聯數組、或映射(map),是一種用於保存鍵值對的抽象數據結構。字典中的每個鍵 key 都是惟一的,經過 key 能夠對值來進行查找或修改。 - - Redis 的字典使用哈希表做爲底層實現

  • 哈希表結構定義
typedef struct dictht{
     //哈希表數組
     dictEntry **table;
     //哈希表大小
     unsigned long size;
     //哈希表大小掩碼,用於計算索引值
     //老是等於 size-1
     unsigned long sizemask;
     //該哈希表已有節點的數量
     unsigned long used;
 
}dictht
複製代碼

  哈希表是由數組 table 組成,table 中每一個元素都是指向 dict.h/dictEntry 結構,dictEntry 結構定義以下:

typedef struct dictEntry{
     //鍵
     void *key;
     //值
     union{
          void *val;
          uint64_tu64;
          int64_ts64;
     }v;
     //指向下一個哈希表節點,造成鏈表
     struct dictEntry *next;
}dictEntry
複製代碼

key 用來保存鍵,val 屬性用來保存值,值能夠是一個指針,也能夠是uint64_t整數,也能夠是int64_t整數。

注意這裏還有一個指向下一個哈希表節點的指針,咱們知道哈希表最大的問題是存在哈希衝突,如何解決哈希衝突,有開放地址法和鏈地址法。這裏採用的即是鏈地址法,經過next這個指針能夠將多個哈希值相同的鍵值對鏈接在一塊兒,用來解決哈希衝突。

Hash表結構示意圖.png

  • 字典特性

  1. 哈希算法:Redis計算哈希值和索引值方法以下:
#一、使用字典設置的哈希函數,計算鍵 key 的哈希值
hash = dict->type->hashFunction(key);
#二、使用哈希表的sizemask屬性和第一步獲得的哈希值,計算索引值
index = hash & dict->ht[x].sizemask;
複製代碼
  1. 解決哈希衝突: 鏈地址法,經過字典裏面的 *next 指針指向下一個具備相同索引值的哈希表節點。
  2. 擴容和收縮:當哈希表保存的簡直對太多或太少的時候,就須要經過rehash(從新散列)來對哈希表進行相應的擴展或者收縮。具體步驟:
    • 3.1. 擴展操做:每次擴展都是根據原哈希表已使用的空間擴大一倍建立另外一個哈希表;若是執行的是收縮操做:每次收縮根據已使用空間縮小一倍建立一個新的哈希表
    • 3.2. 從新計算索引值(哈希算法),將鍵值對應到新的哈希表位置上
    • 3.3. 全部鍵值遷移完成以後,釋放舊哈希表的空間。
  3. 觸發擴容的條件:
    • 4.1. Redis服務目前沒有執行BGSAVE命令或者BGREWRITEAOF命令,而且負載因子>=1
    • 4.2. Redis服務目前正在執行 BGSAVE命令或者BGREWRITEAOF命令,而且負載因子>=5
    • 4.3. 負載因子= 哈希表已保存節點數量 / 哈希表大小
  4. 漸進式rehash: 初始化擴容/收縮以後,查詢在新舊兩個哈希表查詢,新增存放在新的哈希表,在遷移完成以後刪除舊哈希表

4.跳躍表SkipList

跳躍表(skiplist)是一種有序數據結構,它經過在每一個節點中維持多個指向其它節點的指針,從而達到快速訪問節點的目的。

  • 結構定義
//表節點定義
typedef struct zskiplistNode {
     //層
     struct zskiplistLevel{
           //前進指針
           struct zskiplistNode *forward;
           //跨度
           unsigned int span;
     }level[];
 
     //後退指針
     struct zskiplistNode *backward;
     //分值
     double score;
     //成員對象
     robj *obj;
 
} zskiplistNode
複製代碼

多個跳躍表節點構成了一個跳躍表

typedef struct zskiplist{
     //表頭節點和表尾節點
     structz skiplistNode *header, *tail;
     //表中節點的數量
     unsigned long length;
     //表中層數最大的節點的層數
     int level;
 
}zskiplist;
複製代碼
  • 跳躍表特性:
  1. 由不少層結構組成;
  2. 每一層都是一個有序的鏈表,排列順序爲由高層到底層,都至少包含兩個鏈表節點,分別是前面的head節點和後面的nil節點;
  3. 最底層的鏈表包含了全部的元素;
  4. 若是一個元素出如今某一層的鏈表中,那麼在該層之下的鏈表也全都會出現(上一層的元素是當前層的元素的子集);
  5. 鏈表中的每一個節點都包含兩個指針,一個指向同一層的下一個鏈表節點,另外一個指向下一層的同一個鏈表節點;
    image
  • 操做
  • ①、搜索:從最高層的鏈表節點開始,若是比當前節點要大和比當前層的下一個節點要小,那麼則往下找,也就是和當前層的下一層的節點的下一個節點進行比較,以此類推,一直找到最底層的最後一個節點,若是找到則返回,反之則返回空。
  • ②、插入:首先肯定插入的層數,有一種方法是假設拋一枚硬幣,若是是正面就累加,直到碰見反面爲止,最後記錄正面的次數做爲插入的層數。當肯定插入的層數k後,則須要將新元素插入到從底層到k層。
  • ③、刪除:在各個層中找到包含指定值的節點,而後將節點從鏈表中刪除便可,若是刪除之後只剩下頭尾兩個節點,則刪除這一層。
    image

5.壓縮表ZipList

  • 壓縮列表(ziplist)是Redis爲了節省內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構,一個壓縮列表能夠包含任意多個節點(entry),每一個節點能夠保存一個字節數組或者一個整數值。
  • 壓縮列表的原理:壓縮列表並非對數據利用某種算法進行壓縮,而是將數據按照必定規則編碼在一塊連續的內存區域,目的是節省內存。
  • 結構定義
//列表節點
typedef struct ziplistNode{

  // 記錄壓縮列表前一個字節的長度.
  int previous_entry_length;
  // 節點的content的內容類型以及長度.encoding類型一共有兩種,一種字節數組一種是整數,encoding區域長度爲1字節、2字節或者5字節長。
  buf encoding;
  // 節點的內容,節點內容類型和長度由encoding決定。
  buf content;

}

// 壓縮表
typedef struct ziplist{
     //表頭節點和表尾節點
     structz ziplistNode entryX;
     //表中節點的數量
     unsigned long length;
     //表中層數最大的節點的層數
     int zlbytes;
     int zltail;
     int zlen;
     int zlend:

}ziplist;
複製代碼

結構圖

節點數據結構.png
壓縮表ziplist數據結構.png

  • 壓縮表特性
    • 節點數據   ①、previous_entry_ength:記錄壓縮列表前一個字節的長度。previous_entry_ength的長度多是1個字節或者是5個字節,若是上一個節點的長度小於254,則該節點只須要一個字節就能夠表示前一個節點的長度了,若是前一個節點的長度大於等於254,則previous length的第一個字節爲254,後面用四個字節表示當前節點前一個節點的長度。利用此原理即當前節點位置減去上一個節點的長度即獲得上一個節點的起始位置,壓縮列表能夠從尾部向頭部遍歷。這麼作頗有效地減小了內存的浪費。   ②、encoding:節點的encoding保存的是節點的content的內容類型以及長度,encoding類型一共有兩種,一種字節數組一種是整數,encoding區域長度爲1字節、2字節或者5字節長。   ③、content:content區域用於保存節點的內容,節點內容類型和長度由encoding決定。

  • 當有序集合對象同時知足如下兩個條件時,對象使用 ziplist 編碼:
    • 1.保存的元素數量小於128;
    • 2.保存的全部元素長度都小於64字節。
    • 不能知足上面兩個條件的使用 skiplist 編碼。以上兩個條件也能夠經過Redis配置文件zset-max-ziplist-entries 選項和 zset-max-ziplist-value 進行修改。

6.整數集合intset

整數集合intset是Redis用於保存整數值的集合抽象數據類型,他能夠保存類型爲int16_t,int32_t或者int64_t的整數值,而且保證集合中不會出現重複元素。

  • 結構定義
typedef struct intset{
     //編碼方式
     uint32_t encoding;
     //集合包含的元素數量
     uint32_t length;
     //保存元素的數組
     int8_t contents[];
 
}intset;
複製代碼

整數集合的每個元素都是contents數組的一個數據項,他們按照從大到小的順序排列,而且不包含任何重複項 length屬性記錄來contents數組的大小 須要注意的是雖然contents數組聲明爲int_8類型,可是實際上contents數組並不保存任何int_8類型的值,其真正類型由encoding來決定。

  • 升級 當咱們新增的元素類型比原集合類型的長度要大時,須要對數組集合進行升級,才能將新元素放入整數集合中,具體步驟:
  1. 根據新元素的類型,擴展整數集合底層數組的大小/類型,併爲新元素分配空間
  2. 若是擴展了類型,將底層數組如今的元素都轉換爲新元素的類型,並在轉換後放到對應的位置,放置過程當中,維持整個數組元素都是有序的。‘
  3. 將新元素添加到整數集合中
  • 降級 整數集合不支持降級操做,一旦對數組進行來升級,編碼就會一直保持升級後的狀態。
相關文章
相關標籤/搜索