1.Redis 概述
Redis 數據庫裏面的每個鍵值對(key-value) 都是由對象(object)組成的:
數據庫鍵總是一個字符串對象(string object);
數據庫的值則可以是字符串對象、列表對象(list)、哈希對象(hash)、集合對象(set)、有序集合(sort set)對象這五種對象中的其中一種。
2.Redis 底層數據結構
有以下數據類型:
簡單動態字符串(SDS), 鏈表, 字典, 跳躍表, 整數集合, 壓縮列表
2.1 SDS
Redis構建了一種名爲簡單動態字符串的抽象類型,作爲默認字符串表示
結構:
/*
* 保存字符串對象的結構
*/
struct sdshdr {
// buf 中已佔用空間的長度
int len;
// buf 中剩餘可用空間的長度
int free;
// 數據空間
char buf[];
};
SDS 與C字符串的區別:
1.獲取字符串長度(SDS O(1)/C 字符串 O(n))
傳統的C 字符串 使用長度爲N+1 的字符串數組來表示長度爲N 的字符串,所以爲了獲取一個長度爲C字符串的長度,必須遍歷整個字符串。
和C 字符串不同,SDS 的數據結構中,有專門用於保存字符串長度的變量,我們可以通過獲取len 屬性的值,直接知道字符串長度
2.杜絕緩衝區溢出
C 字符串 不記錄字符串長度,除了獲取的時候複雜度高以外,還容易導致緩衝區溢出。
假設程序中有兩個在內存中緊鄰着的 字符串 s1 和 s2,其中s1 保存了字符串“redis”,二s2 則保存了字符串“MongoDb”。將s1 的內容修改爲redis cluster,但是又忘了重新爲s1 分配足夠的空間。那麼s2原來的內容會被覆蓋掉。
Redis 中SDS 的空間分配策略完全杜絕了發生緩衝區溢出的可能性:
當我們需要對一個SDS 進行修改的時候,redis 會在執行拼接操作之前,預先檢查給定SDS 空間是否足夠,如果不夠,會先拓展SDS 的空間,然後再執行拼接操作。
3.減少修改字符串時帶來的內存重分配次數
C語言字符串在進行字符串的擴充和收縮的時候,都會面臨着內存空間的重新分配問題。
SDS在拓展時會進行預分配策略, 通過這種預分配策略,SDS將連續增長N次字符串所需的內存重分配次數從必定N次降低爲最多N次。
4.惰性釋放空間
SDS有free屬性可以記錄剩餘空間的,當對字符串進行收縮的時候,redis只記錄free的值,避免下次修改時,對字符串空間進行拓展。
SDS提供了相應的API,在需要的時候,自行釋放SDS的空餘空間。
5.二進制安全
C 字符串中的字符必須符合某種編碼,並且除了字符串的末尾之外,字符串裏面不能包含空字符,否則最先被程序讀入的空字符將被誤認爲是字符串結尾,這些限制使得C字符串只能保存文本數據,而不能保存想圖片,音頻,視頻,壓縮文件這樣的二進制數據。
但是在Redis中,不是靠空字符來判斷字符串的結束的,而是通過len這個屬性。那麼,即便是中間出現了空字符對於SDS來說,讀取該字符仍然是可以的。
3.鏈表
鏈表提供了高效的節點重排能力,以及順序性的節點訪問方式,並且可以通過增刪節點來靈活地調整鏈表的長度。
鏈表在Redis 中的應用非常廣泛,比如列表鍵的底層實現之一就是鏈表。當一個列表鍵包含了數量較多的元素,又或者列表中包含的元素都是比較長的字符串時,Redis 就會使用鏈表作爲列表鍵的底層實現。
1.鏈表的數據結構ListNode(雙向鏈表):
typedef struct listNode{
struct listNode *prev;
struct listNode * next;
void * value;
}
一般用list操作鏈表:
typedef struct list{
//表頭節點
listNode * head;
//表尾節點
listNode * tail;
//鏈表長度
unsigned long len;
//節點值複製函數
void *(*dup) (void *ptr);
//節點值釋放函數
void (*free) (void *ptr);
//節點值對比函數
int (*match)(void *ptr, void *key);
}
3.鏈表的特性
雙端:鏈表節點帶有prev 和next 指針,獲取某個節點的前置節點和後置節點的時間複雜度都是O(N)
無環:表頭節點的 prev 指針和表尾節點的next 都指向NULL,對立案表的訪問時以NULL爲截止
表頭和表尾:因爲鏈表帶有head指針和tail 指針,程序獲取鏈表頭結點和尾節點的時間複雜度爲O(1)
長度計數器:鏈表中存有記錄鏈表長度的屬性 len
多態:鏈表節點使用 void* 指針來保存節點值,並且可以通過list 結構的dup 、 free、 match三個屬性爲節點值設置類型特定函數。
4.字典
是一種用於保存鍵值對的抽象數據結構。
在字典中,一個鍵(key)可以和一個值(value)進行關聯,字典中的每個鍵都是獨一無二的。在C語言中,並沒有這種數據結構,但是Redis 中構建了自己的字典實現。
1.字典的定義
# 1.哈希表
typedef struct dictht {
//哈希表數組
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩碼,用於計算索引值
unsigned long sizemask;
//該哈希表已有節點的數量
unsigned long used;
}
# 2.哈希表節點
typeof struct dictEntry{
//鍵
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}
struct dictEntry *next;
}
# 3.字典
typedef struct dict {
// 類型特定函數
dictType *type;
// 私有數據
void *privedata;
// 哈希表
dictht ht[2];
// rehash 索引
in trehashidx;
}
1.哈希表 我們可以看到,在結構中存有指向dictEntry 數組的指針,而我們用來存儲數據的空間既是dictEntry
2.哈希表節點 在數據結構中,我們清楚key 是唯一的,但是我們存入裏面的key 並不是直接的字符串,而是一個hash 值,通過hash 算法,將字符串轉換成對應的hash 值,然後在dictEntry 中找到對應的位置。
這時候我們會發現一個問題,如果出現hash 值相同的情況怎麼辦?Redis 採用了鏈地址法(類比於HashMap中的桶):
3.字典 type 屬性 和privdata 屬性是針對不同類型的鍵值對,爲創建多態字典而設置的。
ht 屬性是一個包含兩個項(兩個哈希表)的數組
2.Rehash
隨着對哈希表的不斷操作,哈希表保存的鍵值對會逐漸的發生改變,爲了讓哈希表的負載因子維持在一個合理的範圍之內,我們需要對哈希表的大小進行相應的擴展或者壓縮,這時候,我們可以通過 rehash(重新散列)操作來完成
5.跳錶
跳躍表(skiplist)是一種有序數據結構,它通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。跳躍表是一種隨機化的數據,跳躍表以有序的方式在層次化的鏈表中保存元素,效率和平衡樹媲美 ——查找、刪除、添加等操作都可以在對數期望時間下完成,並且比起平衡樹來說,跳躍表的實現要簡單直觀得多。
Redis 只在兩個地方用到了跳躍表,一個是實現有序集合鍵,另外一個是在集羣節點中用作內部數據結構。
Redis 的跳躍表 主要由兩部分組成:zskiplist(鏈表)和zskiplistNode (節點)
typedef struct zskiplistNode{
//層
struct zskiplistLevel{
//前進指針
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
//後退指針
struct zskiplistNode *backward;
//分值
double score;
//成員對象
robj *obj;
}
typedef struct zskiplist {
//表頭節點和表尾節點
structz skiplistNode *header,*tail;
//表中節點數量
unsigned long length;
//表中層數最大的節點的層數
int level;
}zskiplist;
1、層:level 數組可以包含多個元素,每個元素都包含一個指向其他節點的指針。
2、前進指針:用於指向表尾方向的前進指針
3、跨度:用於記錄兩個節點之間的距離
4、後退指針:用於從表尾向表頭方向訪問節點
5、分值和成員:跳躍表中的所有節點都按分值從小到大排序。成員對象指向一個字符串,這個字符串對象保存着一個SDS值
從結構圖中我們可以清晰的看到,header,tail分別指向跳躍表的頭結點和尾節點。level 用於記錄最大的層數,length 用於記錄我們的節點數量。
總結:
- 跳躍表是有序集合的底層實現之一
- 主要有zskiplist 和zskiplistNode兩個結構組成
- 每個跳躍表節點的層高都是1至32之間的隨機數
- 在同一個跳躍表中,多個節點可以包含相同的分值,但每個節點的對象必須是唯一的
- 節點按照分值的大小從大到小排序,如果分值相同,則按成員對象大小排序
6.整數集合(Intset)
其實就是一個特殊的集合,裏面存儲的數據只能夠是整數,並且數據量不能過大。
typedef struct intset{
//編碼方式
uint32_t enconding;
// 集合包含的元素數量
uint32_t length;
//保存元素的數組
int8_t contents[];
}
整數集合的升級
在上述數據結構圖中我們可以看到,intset 在默認情況下會幫我們設定整數集合中的編碼方式,但是當我們存入的整數不符合整數集合中的編碼格式時,就需要使用到Redis 中的升級策略來解決
Intset 中升級整數集合並添加新元素共分爲三步進行:
1、根據新元素的類型,擴展整數集合底層數組的空間大小,併爲新元素分配空間
2、將底層數組現有的所有元素都轉換成新的編碼格式,重新分配空間
3、將新元素加入到底層數組中
總結
- 整數集合的底層實現爲數組,這個數組以有序,無重複的範式保存集合元素,在有需要時,程序會根據新添加的元素類型改變這個數組的類型
- 升級操作爲整數集合帶來了操作上的靈活性,並且儘可能地節約了內存
- 整數集合只支持升級操作,不支持降級操作
7.壓縮列表
壓縮列表是列表鍵和哈希鍵的底層實現之一。當一個列表鍵只把汗少量列表項,並且每個列表項要麼就是小整數,要麼就是長度比較短的字符串,那麼Redis 就會使用壓縮列表來做列表鍵的底層實現。