Redis的數據類型有string、list、set、zset、hash,那麼這些數據類型底層如何實現的呢?
Redis是用C語言寫的,底層數據結構包括六種:動態字符串、鏈表、字典、跳躍表、整數集合和壓縮列表。
1、動態字符串
struct sdshdr{
//記錄buf數組中已使用字節的數量
//等於 SDS 保存字符串的長度
int len;
//記錄 buf 數組中未使用字節的數量
int free;
//字節數組,用於保存字符串
char buf[];
}
以存儲“Redis”字符串爲例:
Redis的動態字符串相比於C語言的string有如下優勢:
- 獲取字符串長度的時間複雜度爲O(1)
C 語言獲取字符串的長度通常是經過遍歷計數來實現的,時間複雜度爲 O(n),而動態字符串獲取長度只需要讀取 len 屬性,時間複雜度爲 O(1)。Redis中通過 strlen key 命令可以獲取 key 的字符串長度。 - 防止緩衝區溢出
C 語言中使用strcat()函數來進行兩個字符串的拼接,如果沒有分配足夠長度的內存空間會導致緩衝區溢出。而動態字符串在進行字符修改的時候,會首先根據記錄的 len 屬性檢查內存空間是否滿足需求,如果不滿足,會進行相應的空間擴展,然後在進行修改操作,所以不會出現緩衝區溢出。 - 減少修改字符串的內存重新分配次數
C語言由於不記錄字符串的長度,所以如果要修改字符串,必須要重新分配內存,如果沒有重新分配,字符串長度增大時會造成內存緩衝區溢出,字符串長度減小時會造成內存泄露。而動態字符串由於len屬性和free屬性的存在,能很方便的修改字符串,比如增長的字符串長度小於free可以直接進行修改而無需內存重新分配。
2、雙向鏈表
typedef struct listNode{
//前置節點
struct listNode *prev;
//後置節點
struct listNode *next;
//節點的值
void *value;
}listNode
Redis鏈表的特性有:
- 雙向:鏈表具有前置節點和後置節點的引用,獲取這兩個節點時間複雜度都爲O(1)。
- 無環:表頭節點的 prev 指針和表尾節點的 next 指針都指向 NULL,對鏈表的訪問都是以 NULL 結束。
- 鏈表長度計數:通過 len 屬性獲取鏈表長度的時間複雜度爲 O(1)。
- 多態:鏈表節點使用 void* 指針來保存節點值,可以保存各種不同類型的值。
3、字典
Redis字典使用哈希表實現
typedef struct dictht{
//哈希表數組
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩碼,用於計算索引值
//總是等於 size-1
unsigned long sizemask;
//該哈希表已有節點的數量
unsigned long used;
}dictht
typedef struct dictEntry{
//鍵
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
//指向下一個哈希表節點,形成鏈表
struct dictEntry *next;
}dictEntry
key 用來保存鍵,val 屬性用來保存值,值可以是一個指針,也可以是整數。
Redis解決哈希衝突是使用的鏈地址法,當哈希表保存的鍵值對太多或者太少時,就要通過 rerehash(重新散列)來對哈希表進行相應的擴展或者收縮,每次擴展後容量爲原來2倍,或收縮後容量爲原來1/2。
Redis哈希表特性:漸近式 rehash
Redis哈希表的擴容和收縮操作不是一次性完成的,而是多次漸進式完成。
Redis在進行漸進式rehash期間,字典的刪除查找更新等操作可能會在兩個哈希表上進行,第一個哈希表沒有找到,就會去第二個哈希表上進行查找。但是進行增加操作,一定是在新的哈希表上進行的。
4、跳躍表
typedef struct zskiplistNode {
//層
struct zskiplistLevel{
//前進指針
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//後退指針
struct zskiplistNode *backward;
//分值
double score;
//成員對象
robj *obj;
} zskiplistNode
跳躍表是有序集合的底層實現之一,基於多指針有序鏈表實現的,可以看成多個有序鏈表。
- 搜索:從最高層的鏈表節點開始,如果比當前節點要大和比當前層的下一個節點要小,那麼則往下找,也就是和當前層的下一層的節點的下一個節點進行比較,以此類推,一直找到最底層的最後一個節點,如果找到則返回,反之則返回空。
- 插入:首先確定插入的層數,有一種方法是假設拋一枚硬幣,如果是正面就累加,直到遇見反面爲止,最後記錄正面的次數作爲插入的層數。當確定插入的層數k後,則需要將新元素插入到從底層到k層。
- 刪除:在各個層中找到包含指定值的節點,然後將節點從鏈表中刪除即可,如果刪除以後只剩下頭尾兩個節點,則刪除這一層。
5、整數集合
typedef struct intset{
//編碼方式
uint32_t encoding;
//集合包含的元素數量
uint32_t length;
//保存元素的數組
int8_t contents[];
}intset;
整數集合(intset)是Redis用於保存整數值的集合抽象數據類型,它可以保存類型爲int16_t、int32_t 或者int64_t 的整數值,並且保證集合中不會出現重複元素。
- 升級
當新增的元素類型比原集合元素類型的長度要大時,需要對整數集合進行升級,才能將新元素放入整數集合中。
1、根據新元素類型,擴展整數集合底層數組的大小,併爲新元素分配空間。
2、將底層數組現有的所有元素都轉成與新元素相同類型的元素,並將轉換後的元素放到正確的位置,放置過程中,維持整個元素順序都是有序的。
3、將新元素添加到整數集合中(保證有序)。
升級能極大地節省內存。 - 降級
整數集合不支持降級操作,一旦對數組進行了升級,編碼就會一直保持升級後的狀態。
6、壓縮列表
- previous_entry_ength:記錄壓縮列表前一個字節的長度。可能是1個字節或者是5個字節,如果上一個節點的長度小於254,則該節點只需要一個字節就可以表示前一個節點的長度了,如果前一個節點的長度大於等於254,則previous length的第一個字節爲254,後面用四個字節表示當前節點前一個節點的長度。利用此原理即當前節點位置減去上一個節點的長度即得到上一個節點的起始位置,壓縮列表可以從尾部向頭部遍歷。這麼做很有效地減少了內存的浪費。
- encoding:節點的encoding保存的是節點的content的內容類型以及長度,一共有兩種,一種字節數組一種是整數,encoding區域長度爲1字節、2字節或者5字節長。
- content:content區域用於保存節點的內容,節點內容類型和長度由encoding決定。
壓縮列表是爲了節省內存而開發的,由一系列特殊編碼的連續內存塊組成的順序型數據結構,一個壓縮列表可以包含任意多個節點(entry),每個節點可以保存一個字節數組或者一個整數值。
壓縮列表的原理:壓縮列表並不是對數據利用某種算法進行壓縮,而是將數據按照一定規則編碼在一塊連續的內存區域,目的是節省內存。