寫在前
實際上,Redis 數據庫裏面的每個鍵值對(key-value) 都是由對象(object)組成的:
數據庫鍵總是一個字符串對象(string object);
數據庫的值則可以是字符串對象、列表對象(list)、哈希對象(hash)、集合對象(set)、有序集合(sort set)對象這五種對象中的其中一種。
使用OBJECT ENCODING key命令能夠顯示當前數據類型的底層數據結構,整理如下:
- 字符串對象
int :保存的是可以用 long 類型表示的整數值
raw:保存長度大於44字節的字符串/修改後的embstr類型/大小超過Long範圍的int(redis3.2版本之前是39字節,之後是44字節)
embstr :保存長度小於44字節的字符串(redis3.2版本之前是39字節,之後是44字節) - 列表對象
ziplist(壓縮列表) :列表保存元素個數小於512個,每個元素長度小於64字節
linkedlist(雙端鏈表) - 哈希對象
ziplist(壓縮列表):列表保存元素個數小於512個,每個元素長度小於64字節
hashtable - 集合對象
intset(整數集合):集合對象中所有元素都是整數,集合對象所有元素數量不超過512
hashtable - 有序集合對象
ziplist(壓縮列表):保存的元素數量小於128,保存的所有元素長度都小於64字節。
skiplist(跳躍表)
接下來展開介紹Redis中幾種主要的底層數據結構
1. 簡單動態字符串SDS
定義:不同於C語言傳統字符串(即以\0結尾的字符數組)的一種新定義的抽象類型。
/*
* 保存字符串對象的結構
*/
struct sdshdr {
// buf 中已佔用空間的長度
int len;
// buf 中剩餘可用空間的長度
int free;
// 數據空間
char buf[];
};
對於上圖:
1、len 變量,用於記錄buf 中已經使用的空間長度(這裏指出Redis 的長度爲5)
2、free 變量,用於記錄buf 中還空餘的空間(初次分配空間,一般沒有空餘,在對字符串修改的時候,會有剩餘空間出現)
3、buf 字符數組,用於記錄我們的字符串(記錄Redis)
爲什麼不直接使用C語言中傳統字符串?
1.提高獲取字符串長度的效率(O(1)<O(N))
2.杜絕緩衝區溢出(檢查len屬性)
3.減少修改字符串時帶來的內存重分配次數,杜絕內存泄漏(檢查len,free)
4.惰性空間釋放(free屬性)
5. 二進制安全(通過len屬性判斷有效字符串)
6. 兼容部分C字符串函數(以\0結尾)
此外,SDS 除了保存數據庫中的字符串值,還可以作爲緩衝區(buffer):包括 AOF 模塊中的AOF緩衝區以及客戶端狀態中的輸入緩衝區(持久化層面)。
2. 鏈表
定義:鏈表提供了高效的節點重排能力,以及順序性的節點訪問方式,並且可以通過增刪節點來靈活地調整鏈表的長度。鏈表在Redis 中的應用非常廣泛,比如列表鍵的底層實現之一就是鏈表。當一個列表鍵包含了數量較多的元素,又或者列表中包含的元素都是比較長的字符串時,Redis 就會使用鏈表作爲列表鍵的底層實現。
/*
鏈表節點定義*/
typedef struct listNode{
struct listNode *prev;/*前置節點*/
struct listNode * next;/*後置節點*/
void * value; /*節點值*/
}
通過多個 listNode 結構就可以組成鏈表,上圖是一個雙端鏈表。
Redis還提供了操作鏈表的數據結構:
typedef struct list{
//表頭節點
listNode * head;
//表尾節點
listNode * tail;
//鏈表長度
unsigned long len;
//節點值複製函數
void *(*dup) (void *ptr);
//節點值釋放函數
void (*free) (void *ptr);
//節點值對比函數
int (*match)(void *ptr, void *key);
}
特性:
1.雙端:鏈表節點帶有prev 和next 指針,獲取某個節點的前置節點和後置節點的時間複雜度都是O(N)
2.無環:表頭節點的 prev 指針和表尾節點的next 都指向NULL,對立案表的訪問時以NULL爲截止
3.表頭和表尾:因爲鏈表帶有head指針和tail 指針,程序獲取鏈表頭結點和尾節點的時間複雜度爲O(1)
4.長度計數器:鏈表中存有記錄鏈表長度的屬性 len
5.多態:鏈表節點使用 void* 指針來保存節點值,並且可以通過list 結構的dup 、 free、 match三個屬性爲節點值設置類型特定函數。
3. 字典Map
定義:字典又稱爲符號表或者關聯數組、或映射(map),是一種用於保存鍵值對的抽象數據結構。字典中的每一個鍵 key 都是唯一的,通過 key 可以對值來進行查找或修改,Redis中使用hash實現。
/*
hash表結構定義
*/
typedef struct dictht {
//哈希表數組
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩碼,用於計算索引值
unsigned long sizemask;
//該哈希表已有節點的數量
unsigned long used;
}
哈希表是由數組 table 組成,table 中每個元素都是指向 dict.h/dictEntry 結構,dictEntry 結構定義如下:
/*
dictEntry節點結構定義
*/
typedef struct dictEntry{
//鍵
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
//指向下一個哈希表節點,形成鏈表
struct dictEntry *next;
}dictEntry
在數據結構中,我們清楚key 是唯一的,但是我們存入裏面的key 並不是直接的字符串,而是一個hash 值,通過hash 算法,將字符串轉換成對應的hash 值,然後在dictEntry 中找到對應的位置。在這裏我們使用鏈地址法解決衝突。
當k1 和k0 的hash 值相同時,將k1中的next 指向k0 想成一個鏈表(頭插)。
/*
字典定義
*/
typedef struct dict {
// 類型特定函數
dictType *type;
// 私有數據
void *privedata;
// 哈希表
dictht ht[2];
// rehash 索引
in trehashidx;
}
type 屬性 和privdata 屬性是針對不同類型的鍵值對,爲創建多態字典而設置的。ht 屬性是一個包含兩個項(兩個哈希表)的數組
普通狀態下的字典:
擴容和收縮
當哈希表保存的鍵值對太多或者太少時,就要通過 rehash(重新散列)來對哈希表進行相應的擴展或者收縮。具體步驟:
1、如果執行擴展操作,會基於原哈希表創建一個大小等於 ht[0].used*2n 的哈希表(也就是每次擴展都是根據原哈希表已使用的空間擴大一倍創建另一個哈希表)。相反如果執行的是收縮操作,每次收縮是根據已使用空間縮小一倍創建一個新的哈希表。
2、重新利用哈希算法,計算索引值,然後將鍵值對放到新的哈希表位置上。
3、所有鍵值對都遷徙完畢後,釋放原哈希表的內存空間。
觸發擴容的條件
1、服務器目前沒有執行 BGSAVE 命令或者 BGREWRITEAOF 命令,並且負載因子大於等於1。
2、服務器目前正在執行 BGSAVE 命令或者 BGREWRITEAOF 命令,並且負載因子大於等於5。
ps:負載因子 = 哈希表已保存節點數量 / 哈希表大小。
漸近式 rehash
即擴容和收縮操作不是一次性、集中式完成的,而是分多次、漸進式完成的。如果保存在Redis中的鍵值對只有幾個幾十個,那麼 rehash 操作可以瞬間完成,但是如果鍵值對有幾百萬,幾千萬甚至幾億,那麼要一次性的進行 rehash,勢必會造成Redis一段時間內不能進行別的操作。所以Redis採用漸進式 rehash,這樣在進行漸進式rehash期間,字典的刪除查找更新等操作可能會在兩個哈希表上進行,第一個哈希表沒有找到,就會去第二個哈希表上進行查找。但是進行 增加操作,一定是在新的哈希表上進行的。
4. 跳躍表skiplist
跳躍表的漫畫解釋:http://blog.jobbole.com/111731/
定義:跳躍表是一種有序數據結構,它通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。跳躍表是一種隨機化的數據,跳躍表以有序的方式在層次化的鏈表中保存元素,效率和平衡樹媲美 ——查找、刪除、添加等操作都可以在對數期望時間下完成,並且比起平衡樹來說,跳躍表的實現要簡單直觀得多。
/*
跳躍表的節點定義
*/
typedef struct zskiplistNode {
//層
struct zskiplistLevel{
//前進指針
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//後退指針
struct zskiplistNode *backward;
//分值
double score;
//成員對象
robj *obj;
} zskiplistNode
1、層:level 數組可以包含多個元素,每個元素都包含一個指向其他節點的指針。
2、前進指針:用於指向表尾方向的前進指針
3、跨度:用於記錄兩個節點之間的距離
4、後退指針:用於從表尾向表頭方向訪問節點
5、分值和成員:跳躍表中的所有節點都按分值從小到大排序。成員對象指向一個字符串,這個字符串對象保存着一個SDS值
/*
跳躍表定義
*/
typedef struct zskiplist {
//表頭節點和表尾節點
structz skiplistNode *header,*tail;
//表中節點數量
unsigned long length;
//表中層數最大的節點的層數
int level;
}zskiplist;
header,tail分別指向跳躍表的頭結點和尾節點。level 用於記錄最大的層數,length 用於記錄我們的節點數量。
特性:
1.跳躍表是有序集合的底層實現之一
2.主要有zskiplist 和zskiplistNode兩個結構組成
3.每個跳躍表節點的層高都是1至32之間的隨機數
4.在同一個跳躍表中,多個節點可以包含相同的分值,但每個節點的對象必須是唯一的
5.節點按照分值的大小從大到小排序,如果分值相同,則按成員對象大小排序
5. 整數集合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 數組聲明爲 int8_t 類型,但是實際上contents 數組並不保存任何 int8_t 類型的值,其真正類型有 encoding 來決定。
整數集合的升級
當我們新增的元素類型比原集合元素類型的長度要大時,需要對整數集合進行升級,才能將新元素放入整數集合中。具體步驟:
1、根據新元素類型,擴展整數集合底層數組的大小,併爲新元素分配空間。
2、將底層數組現有的所有元素都轉成與新元素相同類型的元素,並將轉換後的元素放到正確的位置,放置過程中,維持整個元素順序都是有序的。
3、將新元素添加到整數集合中(保證有序)。
優點:提升靈活性,節約內存。
特性:
1.整數集合是集合建的底層實現之一
2.整數集合的底層實現爲數組,這個數組以有序,無重複的範式保存集合元素,在有需要時,程序會根據新添加的元素類型改變這個數組的類型
3.升級操作爲整數集合帶來了操作上的靈活性,並且儘可能地節約了內存
4.整數集合只支持升級操作,不支持降級操作
6. 壓縮列表ziplist
定義:壓縮列表是Redis爲了節省內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構,一個壓縮列表可以包含任意多個節點(entry),每個節點可以保存一個字節數組或者一個整數值。
壓縮列表的原理:壓縮列表並不是對數據利用某種算法進行壓縮,而是將數據按照一定規則編碼在一塊連續的內存區域,目的是節省內存。
壓縮列表的組成:
1、zlbytes:用於記錄整個壓縮列表佔用的內存字節數
2、zltail:記錄要列表尾節點距離壓縮列表的起始地址有多少字節
3、zllen:記錄了壓縮列表包含的節點數量。
4、entryX:要說列表包含的各個節點
5、zlend:用於標記壓縮列表的末端
①previous_entry_ength:記錄壓縮列表前一個字節的長度
②encoding:節點的encoding保存的是節點的content的內容類型以及長度,encoding類型一共有兩種,一種字節數組一種是整數,encoding區域長度爲1字節、2字節或者5字節長
③content:content區域用於保存節點的內容,節點內容類型和長度由encoding決定
參考:
https://www.cnblogs.com/ysocean/p/9102811.html
https://www.cnblogs.com/jaycekon/p/6227442.html