Redis(一):Redis的5種資料型別
技術標籤:Redis
Redis總共有5種資料型別,分別是字串物件,連結串列物件,雜湊物件,集合物件,有序集合物件。Redis是一個鍵值對資料庫,Redis的鍵總是一個字串物件,Redis的值可以為連結串列、雜湊、集合、有序集合物件等。
1、字串物件
Redis自己構建了一種名為簡單動態字串(SDS)的抽象型別,SDS定義在sds.h中,SDS的各種API在sds.c中實現。
/* * 儲存字串物件的結構 */ struct sdshdr { // buf 中已佔用空間的長度 int len; // buf 中剩餘可用空間的長度 int free; // 資料空間 char buf[]; };
SDS與C字串的區別:
- 常數複雜度獲取字串的長度,SDS的字串長度可以通過len屬性獲取。
- 杜絕緩衝區溢位,SDS的API需要對SDS進行修改時,會檢查SDS的空間是否滿足修改所需的要求,如果不滿足,API會自動將SDS空間擴充套件至執行修改所需的大小。
- 減少修改字串帶來的記憶體重新分配次數:
1)通過空間預分配,可以減少字串拼接時記憶體重分配的次數。當SDS的長度小於1MB時,分配兩倍和len屬性同樣大小的未使用空間。如果SDS的長度大於1MB時,將分配1MB的未使用空間。
2)通過惰性空間釋放,可以減少字串縮短時的記憶體回收的次數。SDS提供了相應的API,讓我們在有需要時,真正釋放SDS的未使用空間。
- 二進位制安全,SDS的API都會以二進位制的方式來處理buf陣列中的內容,因此SDS不僅可以儲存文字,還可以儲存二進位制。而對於C的字串來說,遇到空格就終止了。
2、連結串列物件
連結串列內建在許多高階語言中,如C++,但是Redis使用C語言編寫,沒有內建這種資料結構,所以Redis構建了自己的連結串列實現。Redis的連結串列和連結串列節點定義在adlist.h中,連結串列的API函式在adlist.c中實現。
typedef struct listNode { // 前置節點 struct listNode *prev; // 後置節點 struct listNode *next; // 節點的值 void *value; } listNode; /* * 雙端連結串列結構 */ 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;
dup、free和match成員是用於實現多型連結串列所需的型別特定函式。Redis的連結串列實現特性可以總結如下:
- 雙端:連結串列節點帶有prev和next指標
- 無環
- 帶表頭指標和表尾指標
- 帶連結串列長度計數器
- 多型 :連結串列節點使用void *指標來儲存節點值,並且可以通過list結構的dup、free和match三個屬性為節點值設定型別特定函式,所以連結串列可以用於儲存各種不同型別的值。
3、雜湊物件
字典經常作為一種資料結構內建在高階程式語言中,如C++,但是Redis使用C語言編寫,並沒有內建這種資料結構,因此Redis構建了自己的字典實現。字典物件的定義在dict.h中,字典物件的API實現在dict.c中。
/*
* 雜湊表
*
* 每個字典都使用兩個雜湊表,從而實現漸進式 rehash 。
*/
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_t u64;
int64_t s64;
} v;
// 指向下個雜湊表節點,形成連結串列
struct dictEntry *next;
} dictEntry;
Reids的字典使用雜湊表作為底層實現,一個雜湊表裡面可以有多個雜湊表節點,而每個雜湊表節點則儲存了字典中的一個鍵值對。
- dictht 中size屬性記錄了雜湊表的大小,也即是table陣列的大小,而used屬性則記錄了雜湊表目前已有節點的數量。
- dictEntry中key屬性儲存鍵值對的鍵,v屬性儲存鍵值對的值,鍵值對的值可以是一個指標,或者是一個uint64_t整數,又或者是int64_t整數。
/*
* 字典
*/
typedef struct dict {
// 型別特定函式
dictType *type;
// 私有資料
void *privdata;
// 雜湊表
dictht ht[2];
// rehash 索引
// 當 rehash 不在進行時,值為 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在執行的安全迭代器的數量
int iterators; /* number of iterators currently running */
} dict;
/*
* 字典型別特定函式
*/
typedef struct dictType {
// 計算雜湊值的函式
unsigned int (*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中type屬性是一個指向dictType結構的指標,每個dictType結構儲存了一簇用於操作特性型別鍵值對的函式,Redis會為不同的字典設定不同的型別特定函式。
- dict中的privdata屬性則儲存了需要傳給那些型別特定函式的可選引數。
- ht陣列中每個項都是一個dictht雜湊表,一般情況下,字典只使用ht[0]雜湊表,ht[1]雜湊表只會在對ht[0]雜湊表進行rehash時使用。
- rehashidx記錄了rehash目前的進度,如果目前沒有進行rehash,那麼它的值為-1。
(1) 雜湊演算法:
第一步,計算雜湊值:hash = dict->type->hashFunction(key);
第二步,根據雜湊值和sizemask的值計算索引值:index = hash & dict->ht[x].sizemask;
(2)解決衝突:雜湊表使用鏈地址法解決衝突
(3)rehash:為了讓雜湊表的負載因子(load_factor)維持在一個合理的範圍內,當雜湊表儲存的鍵值對數量太多或者太少時,程式需要對雜湊表的大小進行相應的擴充套件或者收縮。
load_factor = ht[0].used / ht[0].size
當一下條件中的任意一個被滿足時,程式會自動開始對雜湊表執行擴充套件操作:
- 伺服器目前沒有執行BGSAVE命令或者BGREWRITEAOF命令,並且雜湊表的負載因子大於等於1.
- 伺服器目前正在執行BGSAVE命令或者BGREWRITEAOF命令,並且雜湊表的負載因子大於等於5.
- 當雜湊表的負載因子小於0.1時,程式自動開始對雜湊表執行收縮操作。
rehash的操作步驟如下:
- 為字典ht[1]雜湊表分配空間,這個空間大小取決於要執行的操作,以及ht[0]當前包含的鍵值對數量。
- 將儲存在ht[0]中的所有鍵值對rehash到ht[1]上面:rehash是指重新計算鍵的雜湊值和索引,然後將鍵值對放在ht[1]雜湊表指定的位置上。
- ht[0]全部遷移到ht[1]之後,釋放ht[0],將ht[1]設定為ht[0],並在ht[1]新建立一個空白雜湊表。
(4)漸進式rehash:ht[0]的鍵值對rehash到ht[1]的動作不是一次性、集中式的完成,而是分多次、漸進式的完成的。這是因為如果一個雜湊表儲存的鍵值對過多,一次性rehash會導致伺服器在一段時間內停止服務。漸進式rehash的步驟如下:
- 為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]
- 把字典中的rehashidx設定為0,表示rehash工作正式開始
- 在rehash進行期間,每次對字典執行新增、刪除、查詢或者更新操作時,程式除了執行的操作外,還會順帶將ht[0]雜湊表在rehashidx索引上的所有鍵值對rehash到ht[1],當rehash工作完成後,程式將rehashidx屬性的值增一。
- 隨著字典操作的不斷執行,最終ht[0]的所有鍵值對都會rehash到ht[1],這時程式將rehashidx的值設定為-1,表示rehash完成。
- 在漸進式rehash的過程中,字典的刪除、查詢、更新等操作會在兩個雜湊表上執行。新新增到字典的鍵值對一律會被儲存到ht[1]裡面,而ht[0]則不再進行任何新增操作。這一措施保證了ht[0]所包含的鍵值對數量只減不增,並且最終變成空表。
4、集合物件
整數集合intset是Redis用於儲存整數值的集合抽象資料型別,它可以儲存型別為int16_t、int32_t、int64_t的整數值,並且保證集合中不會出現重複元素。整數集合定義在intset.h中,整數集合的API實現在intset.c中。
typedef struct intset {
// 編碼方式
uint32_t encoding;
// 集合包含的元素數量
uint32_t length;
// 儲存元素的陣列
int8_t contents[];
} intset;
- intset中length記錄了整數集合包含的元素的數量,也是contents陣列的長度。雖然contents屬性宣告為int8_t型別的陣列,但實際上contents陣列並不儲存任何int8_t型別的值,contents陣列的真正型別取決於encoding屬性的值。encoding可以取值為:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64。
- 升級:當我們將一個新元素新增到整數集合裡面,並且新元素型別比整數集合現有的所有元素型別都要長時,整數集合需要先進行升級,然後才能將新元素新增到整數集合裡面。升級提升了靈活性並且節約了記憶體,整數集合不支援降級操作。升級分為三步:
1) 根據新元素型別,擴充套件整數集合底層陣列的空間大小,併為新元素分配空間。
2) 轉換原有元素的資料型別,並將轉換後的元素放在正確的位置上,放置的過程中需要維持底層陣列的有序性質不變。
3) 將新元素新增到底層數組裡面。
5、有序集合物件
跳躍表是一種有序資料結構,支援平均O(logN)、最壞O(N)複雜度的節點查詢,在大部分情況下,跳躍表的效率可以和平衡樹相媲美,並且因為跳躍表的實現比平衡樹更簡單,所以不少程式使用跳躍表來代替平衡樹。跳躍表的實現定義在redis.h中,實現在redis.c中。
/*
* 跳躍表節點
*/
typedef struct zskiplistNode {
// 成員物件
robj *obj;
// 分值
double score;
// 後退指標
struct zskiplistNode *backward;
// 層
struct zskiplistLevel {
// 前進指標
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
/*
* 跳躍表
*/
typedef struct zskiplist {
// 表頭節點和表尾節點
struct zskiplistNode *header, *tail;
// 表中節點的數量
unsigned long length;
// 表中層數最大的節點的層數
int level;
} zskiplist;
- zskiplist 中 header指向跳躍表頭節點,tail指向跳躍表尾節點,level記錄了目前跳躍表內,層數最大的那個節點的層數(表頭節點的層數不計算在內),length記錄了跳躍表的長度,也即是跳躍表目前包含的節點數量。
- zskiplistNode包含了4個屬性,分別是層(level)、後退(backward)、分值(score)、成員物件(obj):