前言
Redis是一個KV數據庫,常用於實現緩存,因爲基於內存實現,所以速度極快。最近閱讀《Redis設計與實現》一書,整理幾篇文章,本文介紹Redis數據結構相關內容。
I. 數據結構
我們通常說的Redis支持的數據類型有五種,包括字符串、哈希、列表、集合、有序集合,其實這只是存儲的數據類型,底層用於存儲數據的數據結構並不是這些,而是動態字符串(SDS)、鏈表、字典(哈希表)、跳躍表、整數集合、壓縮列表。
Redis的每一種數據類型其底層所用的數據結構並不只有一種,往往會在合適的情況下采用合適的數據結構,也是基於性能考慮。對照下面導圖,我們可以簡單的瞭解每種數據類型有哪些數據結構實現方式。
下面具體介紹Redis中定義的幾種數據結構。
動態字符串 SDS
Redis中自定義了一個動態字符串取代了C語言中的字符串作爲默認字符串數據結構,凡是可能會變化的字符串Redis都會用SDS作爲實現。
用途:
- KV中鍵和值都會用
- 緩衝區:AOF緩衝區,客戶端狀態中的輸入緩衝區
定義——ArrayList的感覺:
struct sdshdr {
int len; // 實際buf使用長度
int free; // 剩餘長度
char buf[]; // 存儲字節數據數組
}
優勢:
-
記錄了長度信息,獲取長度複雜度
-
自動擴容機制——緩衝區不會溢出(莫名其妙更改了別的內存空間數據)
-
空間預分配和惰性空間釋放——減少增減字符串時帶來的內存重分配次數
-
以
len
來判斷數據結束——不僅僅可以存儲字符,其實可以是二進制數據
雙端鏈表 LinkedList
Redis定義的雙端鏈表其實也很正常,就是一個能夠雙向遍歷,含有頭尾指針的鏈表數據結構,當然也就具有鏈表的優勢與劣勢——增刪快,查詢慢。
用途:
- 列表底層實現之一(數量比較多或者元素都是比較長的字符串)
- 發佈訂閱、慢查詢、監視器等功能
- Redis保存多個客戶端的狀態信息
- 客戶端輸出緩衝區
節點定義——雙向鏈表節點:
typedef struct listNode{
struct listNode *prev;
struct listNode *next;
void *value;
}
鏈表定義——雙端雙向鏈表:
typedef struct list{
listNode *head;
listNode *tail;
unsigned long len;
void *(*dup) (void *ptr); // 節點值複製函數
void (*free) (void *ptr); // 節點釋放函數
int (*match) (void *ptr, void *key); // 節點值對比函數
}
優勢:
- 雙端雙向——前後節點查詢複雜度
- 無環——首尾指針的指向都是
NULL
- 長度計數器
len
——獲取列表元素數量複雜度 - 多態——節點使用
void*
指針保存節點值,可以是多態的
字典 HashTable
字典的實現也是一個Key-Value的映射,也就是Map數據結構,實現方法熟知的就是Java裏的 HashMap
和 TreeMap
。Redis實現的方式是 HashMap
,也就是利用hash表的方式。
用途:
- Redis數據庫的實現,對於數據的增刪改查都是基於字典的操作
- 哈希類型的底層實現之一
哈希節點定義——哈希表中的每個元素:
typedef struct dicEntry {
void *key; // 鍵
union {
void *val; // 指針
uint64_t u64; // uint64_t整數
int64_t s64; // int64_t整數
} v; // 值有三種形式
struct dicEntry *next; // 指向下一個哈希節點,拉鍊法解決哈希碰撞
} dicEntry;
哈希表定義——存儲數據元素:
typedef struct dictht {
dicEntry **table; // 哈希數組
unsigned long size; // 數組長度,會擴容的
unsigned long sizemask; // 哈希表大小掩碼,用於計算索引值,等於size-1
unsigned long used; // 已經使用的數量
} dicht;
字典定義:
typedef struct dict {
dictType *type; // 保存了一簇用於操作特定類型鍵值對的函數,Redis會爲不同的字典設置不同的dictType
void *privdata; // 私有數據。
dictht ht[2]; // 哈希表數組,兩個哈希表,ht[1]專爲擴容縮容使用
long rehashidx; // 用於記錄rehash進度,-1表示結束
} dict;
字典類型定義:
typedef struct dictType {
unsigned int (*hashFunction)(const void *key); // hash函數
void *(*keyDup)(void *privdata, const void *key); // key複製
void *(*valDup)(void *privdata, const void *obj); // val複製函數
int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 兩個key值比較
void (*keyDestructor)(void *privdata, void *key); // key的析構函數
void (*valDestructor)(void *privdata, void *obj); // val的析構函數
} dictType;
特性:
關於 HashMap
這種數據結構,其需要關注的幾個特性相信都是老生常談:
- 哈希碰撞:Redis利用頭插拉鍊法解決碰撞問題(
dicEntry
的next
指針用於拉鍊) - 擴容縮容機制:
- Redis爲每個字典定義了兩個哈希表,0號表是用於存數據的,1號表用於擴容縮容時使用的
- 擴容量: 縮容量:
- 分次漸進式的遷移,利用
rehashidx
來記錄h[0]遷移h[1]數據的進度,-1表示結束,≥0表示哈希到ht[0]的下標進度 - 擴容縮容期間,遷移數據的工作平均分攤到每次對字典的增刪改查操作中,防止長時間rehash,Redis不響應
- 擴容縮容時機:
- 負載因子:哈希表已有節點數量/哈希表大小 (因爲哈希碰撞,所以會大於1)
- 服務器沒有在執行 BGSAVE 或 BGREWRITEAOF 命令,且負載因子≥1
- 或者服務器正在執行 BGSAVE 或 BGREWRITEAOF 命令,但負載因子已經≥5
跳躍表 SkipList
跳躍表是種有序的數據結構,其本質是加速有序鏈表的插入刪除速度,其實鏈表插入刪除如果不是在頭尾的話,首先需要定位,定位的複雜度就不是了。對於跳躍表,不熟悉的可以參考:以後有面試官問你跳躍表,你就把這篇文章扔給他
用途:
- 實現有序集合類型
跳躍表節點定義:
typedef struct zskiplistNode {
struct zskiplistNode *backward; // 後退指針,也就是支持逆向遍歷
double score; // 分值
robj *obj; // 成員對象,也就是跳躍表該節點保存的數據
// 該節點的層數組,表示該節點定義多少層
struct zskiplistLevel {
struct zskiplistNode *forward; // 每層中包含前向指針
unsigned int span; // 跨度,表示前進幾步到下一個節點
} level[];
}
跳躍表定義:
一個跳躍表是由一個個節點順序連接而成。
typedef struct zskiplist {
/* 跳躍表的頭結點和尾節點,尾節點的存在主要是爲了能夠快速定位出當前跳躍表的最後一個節點,實現反向遍歷 */
struct zskiplistNode *header, *tail;
/* 當前跳躍表的長度,保留這個字段的主要目的是可以再O(1)時間內獲取跳躍表的長度 */
unsigned long length;
/* 所有節點中level最大值,不包括頭結點(頭結點的level永遠都是最大的值---ZSKIPLIST_MAXLEVEL = 32)。level的值隨着跳躍表中節點的插入和刪除隨時動態調整 */
int level;
} zskiplist;
特性:
- 加速了有序鏈表的增刪改查複雜度,都爲
- 統計排位有
span
的加速,統計個數有length
的加速 - 每個節點的層數都是 1~32 的隨機數
- 分值相同的節點按成員對象的大小進行排序
整數集合 intset
整數集合其實就是用於保存整數數值的有序無重複集合,其實現方式底層就是數組,數組類型則可以是 int16_t
、int32_t
和 int64_t
,數據類型會自動升級。
用途:
- 實現整數數值的集合數據類型
整數集合定義——數組實現的Set:
typedef struct intset {
uint32_t encoding; // 編碼方式,其實就是標識當前的整數集合類型(有符號16位、32位和64位)
uint32_t length; // 集合中元素數量
int8_t contents[]; // 保存整數數據,從小到大有序排列
} intset;
特性:
- 類型升級
- 新插入元素如果帶來類型升級,那麼必然其插入位置爲頭或者尾
- 升級時,先分配好需要的內存空間,然後按原數組順序拷貝數組,最後再插入新元素
- 有了自動類型升級,我們可以靈活的添加三種類型的集合不受限制。同時節約了內存,按需分配。
- 類型降級——不支持降級
壓縮列表 ZipList
壓縮列表的本質是一系列特殊編碼的連續內存塊組成的順序性數據結構,其實是Redis爲了節約內存而設計的數據結構。一個壓縮列表可以包含任意多個節點,每個節點可以保存一個字節數組或者一個整數值。
用途:
- 實現列表數據類型
- 實現哈希數據類型
Entry節點定義:
struct entry {
int<var> prevlen; // 前一個節點的佔用字節長度,prevlen屬性的長度可以是1字節或5字節(取決於前一個節點字節長度是否小於254字節),
int<var> encoding; // 元素類型編碼,保存了數據的類型和長度
optional byte[] content; // 元素內容,可以是字節數組,或者是整數類型數據
}
壓縮列表定義:
struct ziplist<T> {
int32 zlbytes; // 整個壓縮列表佔用字節數
int32 zltail_offset; // 最後一個元素距離壓縮列表起始位置的偏移量,用於快速定位到最後一個節點
int16 zllength; // 元素個數,當該值等於65535(可能發生溢出)時,需要遍歷才能確定真實元素個數
T[] entries; // 元素內容列表,挨個緊湊存儲
int8 zlend; // 標誌壓縮列表的結束,值恆爲0xFF(十進制255)
}
特性:
- 壓縮列表逆向遍歷——利用
prevlen
屬性可以快速的定位前一節點的地址 - 由於
prevlen
屬性的長度依賴前一個節點的具體長度,本身可以是1字節或者5字節。如果其中一個節點的長度更改,引發後繼節點的prevlen
屬性從1字節變爲5字節,進而又導致後繼的後繼也需要調整…這就是連鎖更新的問題。但其實也無大礙,實際中很難有這種極端的連鎖下去的情況。