說到Redis的數據結構,咱們大概會很快想到Redis的5種常見數據結構:字符串(String)、列表(List)、散列(Hash)、集合(Set)、有序集合(Sorted Set),以及他們的特色和運用場景。不過它們是Redis對外暴露的數據結構,用於API的操做,而組成它們的底層基礎數據結構又是什麼呢git
Redis的GitHub地址github.com/antirez/red…github
Redis是用C語言寫的,可是Redis並無使用C的字符串表示(C是字符串是以\0
空字符結尾的字符數組),而是本身構建了一種簡單動態字符串(simple dynamic string,SDS)的抽象類型,並做爲Redis的默認字符串表示redis
在Redis中,包含字符串值的鍵值對底層都是用SDS實現的算法
SDS的結構定義在sds.h
文件中,SDS的定義在Redis 3.2版本以後有一些改變,由一種數據結構變成了5種數據結構,會根據SDS存儲的內容長度來選擇不一樣的結構,以達到節省內存的效果,具體的結構定義,咱們看如下代碼數據庫
// 3.0
struct sdshdr {
// 記錄buf數組中已使用字節的數量,即SDS所保存字符串的長度
unsigned int len;
// 記錄buf數據中未使用的字節數量
unsigned int free;
// 字節數組,用於保存字符串
char buf[];
};
// 3.2
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
複製代碼複製代碼
3.2版本以後,會根據字符串的長度來選擇對應的數據結構編程
static inline char sdsReqType(size_t string_size) {
if (string_size < 1<<5) // 32
return SDS_TYPE_5;
if (string_size < 1<<8) // 256
return SDS_TYPE_8;
if (string_size < 1<<16) // 65536 64k
return SDS_TYPE_16;
if (string_size < 1ll<<32) // 4294967296 4G
return SDS_TYPE_32;
return SDS_TYPE_64;
}
複製代碼複製代碼
下面以3.2版本的sdshdr8
看一個示例數組
len
:記錄當前已使用的字節數(不包括'\0'
),獲取SDS長度的複雜度爲O(1)alloc
:記錄當前字節數組總共分配的字節數量(不包括'\0'
)flags
:標記當前字節數組的屬性,是sdshdr8
仍是sdshdr16
等,flags值的定義能夠看下面代碼buf
:字節數組,用於保存字符串,包括結尾空白字符'\0'
// flags值定義
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
複製代碼複製代碼
上面的字節數組的空白處表示未使用空間,是Redis優化的空間策略,給字符串的操做留有餘地,保證安全提升效率緩存
C語言使用長度爲N+1的字符數組來表示長度爲N的字符串,字符數組的最後一個元素爲空字符'\0'
,可是這種簡單的字符串表示方法並不能知足Redis對於字符串在安全性、效率以及功能方面的要求,那麼使用SDS,會有哪些好處呢安全
參考於《Redis設計與實現》bash
常數複雜度獲取字符串長度
C字符串不記錄字符串長度,獲取長度必須遍歷整個字符串,複雜度爲O(N);而SDS結構中自己就有記錄字符串長度的len
屬性,全部複雜度爲O(1)。Redis將獲取字符串長度所需的複雜度從O(N)降到了O(1),確保獲取字符串長度的工做不會成爲Redis的性能瓶頸
杜絕緩衝區溢出,減小修改字符串時帶來的內存重分配次數
C字符串不記錄自身的長度,每次增加或縮短一個字符串,都要對底層的字符數組進行一次內存重分配操做。若是是拼接append操做以前沒有經過內存重分配來擴展底層數據的空間大小,就會產生緩存區溢出;若是是截斷trim操做以後沒有經過內存重分配來釋放再也不使用的空間,就會產生內存泄漏
而SDS經過未使用空間解除了字符串長度和底層數據長度的關聯,3.0版本是用free
屬性記錄未使用空間,3.2版本則是alloc
屬性記錄總的分配字節數量。經過未使用空間,SDS實現了空間預分配和惰性空間釋放兩種優化的空間分配策略,解決了字符串拼接和截取的空間問題
二進制安全
C字符串中的字符必須符合某種編碼,除了字符串的末尾,字符串裏面是不能包含空字符的,不然會被認爲是字符串結尾,這些限制了C字符串只能保存文本數據,而不能保存像圖片這樣的二進制數據
而SDS的API都會以處理二進制的方式來處理存放在buf
數組裏的數據,不會對裏面的數據作任何的限制。SDS使用len
屬性的值來判斷字符串是否結束,而不是空字符
兼容部分C字符串函數
雖然SDS的API是二進制安全的,但仍是像C字符串同樣以空字符結尾,目的是爲了讓保存文本數據的SDS能夠重用一部分C字符串的函數
C字符串與SDS對比
C字符串 | SDS |
---|---|
獲取字符串長度複雜度爲O(N) | 獲取字符串長度複雜度爲O(1) |
API是不安全的,可能會形成緩衝區溢出 | API是安全的,不會形成緩衝區溢出 |
修改字符串長度必然會須要執行內存重分配 | 修改字符串長度N次最多會須要執行N次內存重分配 |
只能保存文本數據 | 能夠保存文本或二進制數據 |
可使用全部<string.h> 庫中的函數 |
可使用一部分<string.h> 庫中的函數 |
鏈表是一種比較常見的數據結構了,特色是易於插入和刪除、內存利用率高、且能夠靈活調整鏈表長度,但隨機訪問困難。許多高級編程語言都內置了鏈表的實現,可是C語言並無實現鏈表,因此Redis實現了本身的鏈表數據結構
鏈表在Redis中應用的很是廣,列表(List)的底層實現就是鏈表。此外,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;
// 節點值複製函數
void *(*dup)(void *ptr);
// 節點值釋放函數
void (*free)(void *ptr);
// 節點值對比函數
int (*match)(void *ptr, void *key);
// 鏈表所包含的節點數量
unsigned long len;
} list;
複製代碼複製代碼
每一個節點listNode
能夠經過prev
和next
指針分佈指向前一個節點和後一個節點組成雙端鏈表,同時每一個鏈表還會有一個list
結構爲鏈表提供表頭指針head
、表尾指針tail
、以及鏈表長度計數器len
,還有三個用於實現多態鏈表的類型特定函數
dup
:用於複製鏈表節點所保存的值free
:用於釋放鏈表節點所保存的值match
:用於對比鏈表節點所保存的值和另外一個輸入值是否相等鏈表結構圖
prev
和表尾節點的next
都指向NULL,對鏈表的訪問以NULL結束len
屬性,獲取鏈表長度的複雜度爲O(1)void*
指針保存節點值,能夠保存不一樣類型的值字典,又稱爲符號表(symbol table)、關聯數組(associative array)或映射(map),是一種用於保存鍵值對(key-value pair)的抽象數據結構。字典中的每個鍵都是惟一的,能夠經過鍵查找與之關聯的值,並對其修改或刪除
Redis的鍵值對存儲就是用字典實現的,散列(Hash)的底層實現之一也是字典
咱們直接來看一下字典是如何定義和實現的吧
Redis的字典底層是使用哈希表實現的,一個哈希表裏面能夠有多個哈希表節點,每一個哈希表節點中保存了字典中的一個鍵值對
哈希表結構定義,dict.h/dictht
typedef struct dictht {
// 哈希表數組
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩碼,用於計算索引值,等於size-1
unsigned long sizemask;
// 哈希表已有節點的數量
unsigned long used;
} dictht;
複製代碼複製代碼
哈希表是由數組table
組成,table
中每一個元素都是指向dict.h/dictEntry
結構的指針,哈希表節點的定義以下
typedef struct dictEntry {
// 鍵
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指向下一個哈希表節點,造成鏈表
struct dictEntry *next;
} dictEntry;
複製代碼複製代碼
其中key
是咱們的鍵;v
是鍵值,能夠是一個指針,也能夠是整數或浮點數;next
屬性是指向下一個哈希表節點的指針,可讓多個哈希值相同的鍵值對造成鏈表,解決鍵衝突問題
最後就是咱們的字典結構,dict.h/dict
typedef struct dict {
// 和類型相關的處理函數
dictType *type;
// 私有數據
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引,當rehash再也不進行時,值爲-1
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 迭代器數量
unsigned long iterators; /* number of iterators currently running */
} dict;
複製代碼複製代碼
type
屬性和privdata
屬性是針對不一樣類型的鍵值對,用於建立多類型的字典,type
是指向dictType
結構的指針,privdata
則保存須要傳給類型特定函數的可選參數,關於dictType
結構和類型特定函數能夠看下面代碼
typedef struct dictType {
// 計算哈希值的行數
uint64_t (*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;
複製代碼複製代碼
dict
的ht
屬性是兩個元素的數組,包含兩個dictht
哈希表,通常字典只使用ht[0]
哈希表,ht[1]
哈希表會在對ht[0]
哈希表進行rehash
(重哈希)的時候使用,即當哈希表的鍵值對數量超過負載數量過多的時候,會將鍵值對遷移到ht[1]
上
rehashidx
也是跟rehash相關的,rehash的操做不是瞬間完成的,rehashidx
記錄着rehash的進度,若是目前沒有在進行rehash,它的值爲-1
結合上面的幾個結構,咱們來看一下字典的結構圖(沒有在進行rehash)
在這裏,哈希算法和rehash(從新散列)的操做再也不詳細說明,有機會之後單獨介紹
當一個新的鍵值對要添加到字典中時,會根據鍵值對的鍵計算出哈希值和索引值,根據索引值放到對應的哈希表上,即若是索引值爲0,則放到
ht[0]
哈希表上。當有兩個或多個的鍵分配到了哈希表數組上的同一個索引時,就發生了鍵衝突的問題,哈希表使用鏈地址法來解決,即便用哈希表節點的next
指針,將同一個索引上的多個節點鏈接起來。當哈希表的鍵值對太多或太少,就須要對哈希表進行擴展和收縮,經過rehash
(從新散列)來執行
一個普通的單鏈表查詢一個元素的時間複雜度爲O(N),即使該單鏈表是有序的。使用跳躍表(SkipList)是來解決查找問題的,它是一種有序的數據結構,不屬於平衡樹結構,也不屬於Hash結構,它經過在每一個節點維持多個指向其餘節點的指針,而達到快速訪問節點的目的
跳躍表是有序集合(Sorted Set)的底層實現之一,若是有序集合包含的元素比較多,或者元素的成員是比較長的字符串時,Redis會使用跳躍表作有序集合的底層實現
跳躍表其實能夠把它理解爲多層的鏈表,它有以下的性質
那麼如何來理解跳躍表呢,咱們從最底層的包含全部元素的鏈表開始,給定以下的鏈表
而後咱們每隔一個元素,把它放到上一層的鏈表當中,這裏我把它叫作上浮(注意,科學的辦法是拋硬幣的方式,來決定元素是否上浮到上一層鏈表,我這裏先簡單每隔一個元素上浮到上一層鏈表,便於理解),操做完成以後的結構以下
查找元素的方法是這樣,從上層開始查找,大數向右找到頭,小數向左找到頭,例如我要查找17
,查詢的順序是:13 -> 46 -> 22 -> 17;若是是查找35
,則是 13 -> 46 -> 22 -> 46 -> 35;若是是54
,則是 13 -> 46 -> 54
上面是查找元素,若是是添加元素,是經過拋硬幣的方式來決定該元素會出現到多少層,也就是說它會有 1/2的機率出現第二層、1/4 的機率出如今第三層......
跳躍表節點的刪除和添加都是不可預測的,很難保證跳錶的索引是始終均勻的,拋硬幣的方式可讓大致上是趨於均勻的
假設咱們已經有了上述例子的一個跳躍表了,如今往裏面添加一個元素18
,經過拋硬幣的方式來決定它會出現的層數,是正面就繼續,反面就中止,假如我拋了2次硬幣,第一次爲正面,第二次爲反面
跳躍表的刪除很簡單,只要先找到要刪除的節點,而後順藤摸瓜刪除每一層相同的節點就行了
跳躍表維持結構平衡的成本是比較低的,徹底是依靠隨機,相比二叉查找樹,在屢次插入刪除後,須要Rebalance來從新調整結構平衡
Redis的跳躍表實現是由redis.h/zskiplistNode
和redis.h/zskiplist
(3.2版本以後redis.h改成了server.h)兩個結構定義,zskiplistNode
定義跳躍表的節點,zskiplist
保存跳躍表節點的相關信息
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
// 成員對象 (robj *obj;)
sds ele;
// 分值
double score;
// 後退指針
struct zskiplistNode *backward;
// 層
struct zskiplistLevel {
// 前進指針
struct zskiplistNode *forward;
// 跨度
// 跨度其實是用來計算元素排名(rank)的,在查找某個節點的過程當中,將沿途訪過的全部層的跨度累積起來,獲得的結果就是目標節點在跳躍表中的排位
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
// 表頭節點和表尾節點
struct zskiplistNode *header, *tail;
// 表中節點的數量
unsigned long length;
// 表中層數最大的節點的層數
int level;
} zskiplist;
複製代碼複製代碼
zskiplistNode
結構
level
數組(層):每次建立一個新的跳錶節點都會根據冪次定律計算出level數組的大小,也就是次層的高度,每一層帶有兩個屬性-前進指針和跨度,前進指針用於訪問表尾方向的其餘指針;跨度用於記錄當前節點與前進指針所指節點的距離(指向的爲NULL,闊度爲0)backward
(後退指針):指向當前節點的前一個節點score
(分值):用來排序,若是分值相同當作員變量在字典序大小排序obj
或ele
:成員對象是一個指針,指向一個字符串對象,裏面保存着一個sds;在跳錶中各個節點的成員對象必須惟一,分值能夠相同zskiplist
結構
header
、tail
表頭節點和表尾節點length
表中節點的數量level
表中層數最大的節點的層數假設咱們如今展現一個跳躍表,有四個節點,節點的高度分別是二、一、四、3
zskiplist
的頭結點不是一個有效的節點,它有ZSKIPLIST_MAXLEVEL層(32層),每層的forward
指向該層跳躍表的第一個節點,若沒有則爲NULL,在Redis中,上面的跳躍表結構以下
整數集合(intset)是Redis用於保存整數值的集合抽象數據結構,能夠保存類型爲int16_t、int32_t、int64_t的整數值,而且保證集合中不會出現重複元素
整數集合是集合(Set)的底層實現之一,若是一個集合只包含整數值元素,且元素數量很少時,會使用整數集合做爲底層實現
整數集合的定義爲inset.h/inset
typedef struct intset {
// 編碼方式
uint32_t encoding;
// 集合包含的元素數量
uint32_t length;
// 保存元素的數組
int8_t contents[];
} intset;
複製代碼複製代碼
contents
數組:整數集合的每一個元素在數組中按值的大小從小到大排序,且不包含重複項length
記錄整數集合的元素數量,即contents數組長度encoding
決定contents數組的真正類型,如INTSET_ENC_INT1六、INTSET_ENC_INT3二、INTSET_ENC_INT64當想要添加一個新元素到整數集合中時,而且新元素的類型比整數集合現有的全部元素的類型都要長,整數集合須要先進行升級(upgrade),才能將新元素添加到整數集合裏面。每次想整數集合中添加新元素都有可能會引發升級,每次升級都須要對底層數組已有的全部元素進行類型轉換
升級添加新元素:
整數集合的升級策略能夠提高整數集合的靈活性,並儘量的節約內存
另外,整數集合不支持降級,一旦升級,編碼就會一直保持升級後的狀態
壓縮列表(ziplist)是爲了節約內存而設計的,是由一系列特殊編碼的連續內存塊組成的順序性(sequential)數據結構,一個壓縮列表能夠包含多個節點,每一個節點能夠保存一個字節數組或者一個整數值
壓縮列表是列表(List)和散列(Hash)的底層實現之一,一個列表只包含少許列表項,而且每一個列表項是小整數值或比較短的字符串,會使用壓縮列表做爲底層實現(在3.2版本以後是使用quicklist
實現)
一個壓縮列表能夠包含多個節點(entry),每一個節點能夠保存一個字節數組或者一個整數值
各部分組成說明以下
zlbytes
:記錄整個壓縮列表佔用的內存字節數,在壓縮列表內存重分配,或者計算zlend
的位置時使用zltail
:記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少字節,經過該偏移量,能夠不用遍歷整個壓縮列表就能夠肯定表尾節點的地址zllen
:記錄壓縮列表包含的節點數量,但該屬性值小於UINT16_MAX(65535)時,該值就是壓縮列表的節點數量,不然須要遍歷整個壓縮列表才能計算出真實的節點數量entryX
:壓縮列表的節點zlend
:特殊值0xFF(十進制255),用於標記壓縮列表的末端每一個壓縮列表節點能夠保存一個字節數字或者一個整數值,結構以下
previous_entry_ength
:記錄壓縮列表前一個字節的長度encoding
:節點的encoding保存的是節點的content的內容類型content
:content區域用於保存節點的內容,節點內容類型和長度由encoding決定上面介紹了Redis的主要底層數據結構,包括簡單動態字符串(SDS)、鏈表、字典、跳躍表、整數集合、壓縮列表。可是Redis並無直接使用這些數據結構來構建鍵值對數據庫,而是基於這些數據結構建立了一個對象系統,也就是咱們所熟知的可API操做的Redis那些數據類型,如字符串(String)、列表(List)、散列(Hash)、集合(Set)、有序集合(Sorted Set)
根據對象的類型能夠判斷一個對象是否能夠執行給定的命令,也可針對不一樣的使用場景,對象設置有多種不一樣的數據結構實現,從而優化對象在不一樣場景下的使用效率
類型 | 編碼 | BOJECT ENCODING 命令輸出 | 對象 |
---|---|---|---|
REDIS_STRING | REDIS_ENCODING_INT | "int" | 使用整數值實現的字符串對象 |
REDIS_STRING | REDIS_ENCODING_EMBSTR | "embstr" | 使用embstr編碼的簡單動態字符串實現的字符串對象 |
REDIS_STRING | REDIS_ENCODING_RAW | "raw" | 使用簡單動態字符串實現的字符串對象 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | "ziplist" | 使用壓縮列表實現的列表對象 |
REDIS_LIST | REDIS_ENCODING_LINKEDLIST | '"linkedlist' | 使用雙端鏈表實現的列表對象 |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | "ziplist" | 使用壓縮列表實現的哈希對象 |
REDIS_HASH | REDIS_ENCODING_HT | "hashtable" | 使用字典實現的哈希對象 |
REDIS_SET | REDIS_ENCODING_INTSET | "intset" | 使用整數集合實現的集合對象 |
REDIS_SET | REDIS_ENCODING_HT | "hashtable" | 使用字典實現的集合對象 |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | "ziplist" | 使用壓縮列表實現的有序集合對象 |
REDIS_ZSET | REDIS_ENCODING_SKIPLIST | "skiplist" | 使用跳躍表表實現的有序集合對象 |
參考:《Redis設計與實現》
原文連接:https://juejin.im/post/5d71d3bee51d453b5f1a04f1
聲明:該文徹底轉載,只爲方便下次查找快速,若是侵權請聯繫本人