redis一共有五大經常使用的對象,用type命令便可查看當前鍵對應的對象類型,分別是string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),可是這些只是對外的數據結構,實際上每個對象都有兩到三種不一樣底層數據結構實現,能夠經過object encoding命令查看鍵值對應的底層數據結構實現,redis
下表即爲每種對象所對應的底層數據結構實現。算法
類型 | 編碼 | 底層數據結構 |
---|---|---|
string | int | 整數值 |
string | raw | 簡單動態字符串 |
string | embstr | 用embstr編碼的簡單動態字符串 |
hash | ziplist | 壓縮列表 |
hash | hashtable | 字典 |
list | ziplist | 壓縮列表 |
list | linkedlist | 雙端列表 |
set | intset | 整數集合 |
set | hashtable | 字典 |
zset | ziplist | 壓縮列表 |
zset | skiplist | 跳錶和字典 |
redis並無使用C字符串,而是使用了名爲簡單動態字符串(SDS)的結構,SDS的定義以下:數組
struct sdshdr {
// 記錄 buf 數組中已使用字節的數量
// 等於 SDS 所保存字符串的長度
int len;
// 記錄 buf 數組中未使用字節的數量
int free;
// 字節數組,用於保存字符串
char buf[];
};
複製代碼
那麼redis爲何要使用看起來更佔空間的SDS結構呢?主要有如下幾個緣由:安全
雙端列表做爲一種經常使用的數據結構,當一個list的長度超過512時,那麼redis將使用雙端列表做爲底層數據結構。下面是一個列表節點的定義:bash
typedef struct listNode {
// 前置節點
struct listNode *prev;
// 後置節點
struct listNode *next;
// 節點的值
void *value;
} listNode;
複製代碼
多個列表節點串聯起來即可實現雙端列表。數據結構
typedef struct list {
// 表頭節點
listNode *head;
// 表尾節點
listNode *tail;
// 鏈表所包含的節點數量
unsigned long len;
// 節點值複製函數
void *(*dup)(void *ptr);
// 節點值釋放函數
void (*free)(void *ptr);
// 節點值對比函數
int (*match)(void *ptr, void *key);
} list;
複製代碼
能夠看到雙端列表是一個無環雙端帶表頭表尾節點的鏈表。函數
散列表(Hash table,也叫哈希表),是根據鍵而直接訪問在內存存儲位置的數據結構。也就是說,它經過計算一個關於鍵值的函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數稱作散列函數,存放記錄的數組稱作散列表。性能
當hashtable的類型沒法知足ziplist的條件時(元素類型小於512且全部值都小於64字節時),redis會使用字典做爲hashtable的底層數據結構實現。redis的字典(dict)中維護了兩個哈希表(table),而每一個哈希表包含了多個哈希表節點(entry)。下面分別來介紹這三個對象。優化
typedef struct dictEntry {
// 鍵
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下個哈希表節點,造成鏈表
struct dictEntry *next;
} dictEntry;
複製代碼
typedef struct dictht {
// 哈希表數組
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩碼,用於計算索引值
// 老是等於 size - 1
unsigned long sizemask;
// 該哈希表已有節點的數量
unsigned long used;
} dictht;
複製代碼
typedef struct dict {
// 類型特定函數
dictType *type;
// 私有數據
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 當 rehash 不在進行時,值爲 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
複製代碼
當在哈希表中存取數據時,首先須要用hash算法算出鍵值對中的鍵所對應的hash值,而後再根據根據table數組的大小取模,計算出對應的索引值,再繼續接下來的操做。redis使用了MurmurHash2 算法來計算鍵的哈希值,又使用了快速冪取模算法下降了取模的複雜度。整個過程以下:ui
hash = dict->type->hashFunction(k0);
index = hash & dict->ht[0].sizemask;
複製代碼
當hash衝突發生時則採用鏈地址法解決hash衝突。
當哈希表保存的鍵值對愈來愈多時,哈希表的負載因子(load factor = used / size)愈來愈大, 本來O(1)複雜度的查找也會漸漸趨向於O(N),爲了保證哈希表的負載因子在必定的範圍以內。redis須要動態的調整table數組的大小,其中最重要的即是rehash過程。rehash分如下的幾個步驟:
redis的rehash過程並非一次性集中rehash,而是分批間隔式的,在dict中的rehashidx即是爲此服務。 相較於一次性的rehash,漸進式的rehash多了下面這些步驟:
這是比較典型的分而治之的思想,將一次性集中做業分散,下降了系統的風險。
跳錶的的查找複雜度爲平均O(logN)/最壞O(N)。在不少場合下做爲替代平衡樹的數據結構,在redis中,若是有序集合的屬性不知足ziplist的要求,則將跳錶做爲有序集合的底層實現。
typedef struct zskiplistNode {
// 後退指針
struct zskiplistNode *backward;
// 分值
double score;
// 成員對象
robj *obj;
// 層
struct zskiplistLevel {
// 前進指針
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
複製代碼
typedef struct zskiplist {
// 表頭節點和表尾節點
struct zskiplistNode *header, *tail;
// 表中節點的數量
unsigned long length;
// 表中層數最大的節點的層數
int level;
} zskiplist;
複製代碼
跳躍表中保存了頭尾節點,方便遍歷,還保存了節點的數量,能夠在O(1) 複雜度內返回跳躍表的長度。
當集合的值全爲整數且集合的長度不超過512時,redis採用整數集合做爲集合的底層數據結構。
typedef struct intset {
// 編碼方式
uint32_t encoding;
// 集合包含的元素數量
uint32_t length;
// 保存元素的數組
int8_t contents[];
} intset;
複製代碼
INTSET_ENC_INT16 , contents 就是一個 int16_t 類型的數組(最小值爲 -32,768 ,最大值爲 32,767 )。 INTSET_ENC_INT32 , contents 就是一個 int32_t 類型的數組(最小值爲 -2,147,483,648 ,最大值爲 2,147,483,647 )。 INTSET_ENC_INT64 , contents 就是一個 int64_t 類型的數組(最小值爲 -9,223,372,036,854,775,808 ,最大值爲 9,223,372,036,854,775,807 )。
redis採用多種編碼的方式,主要仍是爲了省內存。當集合中加入了不符合當前集合編碼的數字時,數組集合會自動更新至能匹配到的編碼,值得注意的是,這種升級是不可逆的,只能由小往大,不能降級。如此一來,就可以在存放小數據時,剩下很大的空間,並且也沒必要爲編碼不匹配的事情而煩惱了。
壓縮列表是redis又一個爲了節省內存所作的優化,是list/hash/zset的底層數據結構之一,當數據值不大且數量較低時,redis都會使用壓縮列表。
壓縮列表和雙端列表有些相似,不過一個用指針銜接起來,一個則是用數組和長度銜接起來。下面來看一看壓縮列表節點的定義:
本文對於redis常見的數據結構及其底層實現進行了分析和梳理,但願可以理清這些底層數據結構對於redis高性能的做用和影響。