Redis設計與實現筆記--數據結構

目錄

簡單動態字符串(SDS:simple dynamic string)

應用場景

SDS定義

SDS與C字符串的區別

鏈表

鏈表實現

字典

字典的實現

普通狀態下的字典

哈希算法

Rehash

漸進式Rehash

跳躍表

使用場景

跳躍表實現

整數集合

整數集合的實現

壓縮列表

壓縮列表構成

連鎖更新


簡單動態字符串(SDS:simple dynamic string)

當Redis需要的不僅僅是一個字符串字面量,而是一個可以被修改的字符串時,Redis就會使用SDS來表示字符串值。

應用場景

  1. 包含字符串值的鍵值對在底層都是SDS實現的。
  2. AOF模塊中的AOF緩衝區,以及客戶端狀態中的輸入緩衝區,都是由SDS實現的。

SDS定義

struct sdshdr {
  // 記錄buf數組中已使用字節的數量
  // 等於SDS所保存字符串的長度
  int len;
  //  記錄buf數據中未使用字節的數量
  int free;
  //字節數組,用於保存字符串
  char buf[];
};

注意:SDS遵循C字符串以空字符結尾的慣例,保存空字符串的1字節空間不在len屬性內。並且空字符串分配額外的1字節空間,以及添加空字符到字符串末尾等操作,是由SDS函數自動完成的。遵循該慣例好處是:SDS可以直接重用一部分C字符串函數庫裏面的函數

SDS與C字符串的區別

  1. 常數複雜度獲取字符串長度。確保了獲取字符串長度的工作不會成爲Redis的性能瓶頸
  2. 杜絕緩衝區溢出。如:拼接操作。會先檢查給定SDS空間是否足夠,不夠的話,先擴展空間,再執行拼接操作
  3. 減少修改字符串時帶來的內存重分配次數。C字符串每次增長或縮短,都要進行一次內存重分配。會導致緩衝區溢出或內存泄漏。SDS實現了空間預分配和惰性空間釋放兩種優化策略。
    • 空間預分配:如果SDS長度小於1MB,那麼將分配和len屬性同樣大小的未使用空間。如果長度將大於等於1MB,會分配1MB未使用空間。
    • 惰性空間釋放:用於優化SDS的字符串縮短操作,當縮短SDS保存的字符串時,不立即使用內存重分配縮短多出來的字節,而是使用free將這些字節數量記錄起來,等待將來使用。
  4. 二進制安全。C字符串除了字符串末尾之外,字符串裏面不能包含空字符串。使得C字符串只能保存文本數據,不能保存二進制數據。buf屬性爲字節數組,可以保存一系列二進制數據。通過len屬性值判斷字符串是否結束。
  5. 兼容部分C字符串函數

鏈表

鏈表提供了高效的節點重排能力,以及順序性的節點訪問方式,並且可以通過增刪節點來靈活的調整鏈表的長度。

鏈表實現

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;

字典

字典,又稱爲符號表、關聯數組或映射,是一種保存鍵值對的抽象數據結構。

字典的實現

typedef struct dict {
// 類型特定函數
dictType *type;
// 私有數據
void *privdata;
// 哈希表
dictht ht[2];
// rehash索引
// 當rehash不在進行時,值爲-1
int rehashidx;
} dict;
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;

普通狀態下的字典

哈希算法

當字典被用作數據庫的底層實現,或者哈希鍵的底層實現時,Redis使用MurmurHash2算法來計算鍵的哈希值。這種算法的優點在於,即使輸入的鍵是有規律的,算法仍能給出一個很好的隨機分佈性,並且算法的計算速度也非常快。

Rehash

當哈希表保存的鍵值對數量太多或太少時,程序需要對哈希表的大小進行相應的擴展或者收縮。可以通過中rehash(重新散列)操作來完成。步驟如下:

  1. 爲字典的ht[1]分配空間,如果執行擴展操作,大小設置爲第一個大於等於ht[0].user*2的2的n次冪。如果執行收縮操作,大小設置爲第一個大於等於ht[0].user的2的n次冪
  2. 將保存在ht[0]的所有鍵值對rehash到ht[1]上面。
  3. 釋放ht[0],將ht[1]設置爲ht[0],並在ht[1]新創建一個空白哈希表。

當以下條件中的任意一個被滿足時,程序會自動開始對哈希表進行擴展操作:

  1. 服務器目前沒有在執行BGSAVE或者BGREWRITEAOF命令,並且哈希表的負載因子大於等於1
  2. 服務器目前正在進行BASAVE或者BGREWRITEAOF命令,並且哈希表的負載因子大於等於5

    負載因子公式:負載因子=哈希表已保存節點數量/哈希表大小

當哈希表的負載因子小於0.1時,程序自動對哈希表執行收縮操作。

漸進式Rehash

Rehash動作並不是一次性、集中式地完成的,而是分多次、漸進式地完成的。這樣做是爲了避免龐大的數據量計算可能會導致的服務器在一段時間內停止服務。對服務器性能造成影響。步驟如下:

  1. 爲ht[1]分配空間,讓字段同時持有ht[0]和ht[1]兩個哈希表
  2. 在字典中維持一個索引計數器變量rehashidx,並將它設爲0,表示rehash工作正式開始
  3. 在rehash進行期間,每次對字典執行添加、刪除、查找或者更新操作時,除了執行指定操作外,還會將ht[0]哈希表在rehashidx索引上的所有鍵值對rehash到ht[1],當rehash工作完成之後,程序將rehashidx屬性的值加1。
  4. 隨着字典操作的不斷執行,最終在某個時間點上,ht[0]的所有鍵值對都會rehash至ht[1],這是將rehashidx設爲-1,表示rehash操作完成。

在進行漸進式rehash期間,字典的刪除、查找、更新會在兩個哈希表上進行。如:查找一個鍵,會先在ht[0]裏面進行查找,沒找到,就會繼續在ht[1]裏面進行查找。新添加的鍵值對一律會被保存到ht[1]裏面,ht[0]不再進行任何添加操作。這一措施保證了ht[0]包含的鍵值對數量會只減不增。隨着rehash操作的中而最終變成空表。

跳躍表

跳躍表是一種有序數據結構,通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。支持平均O(logN),最壞O(N)複雜的的節點查找。

使用場景

  • 實現有序集合鍵
  • 集羣節點中用作內部數據結構

跳躍表實現

typedef struct zskiplistNode {
// 層
struct zskiplistLevel {
    // 前進指針
    struct zskiplistNode *forward;
    // 跨度
    unsigned int span;
} level[];

//後退指針
struct zskiplistNode *backward;
// 分值
double score;
// 成員對象
robj *obj;
} zskiplistNode;
typedef struct zskiplist {
// 表頭節點和表尾節點
struct skiplistNode *header,*tail;
// 表中節點數量
unsigned long length;
// 表中層數最大的節點的層數
int level;
} zskiplist;

由多個跳躍節點組成的跳躍表

帶有zskiplist結構的跳躍表

整數集合

當一個集合只包含整數值元素,並且這個集合的元素數量不多時,Redis就會使用整數集合作爲結合鍵的底層實現。

整數集合的實現

typedef struct intset {
// 編碼方式
uint32_t encoding;
// 集合包含的元素數量
uint32_t length;
// 保存元素的數組
int8_t contents[];
} intset;

contents數組按從小到大的順序保存集合中的元素。

壓縮列表

當一個列表鍵只包含少量列表項,並且每個列表項要麼就是小整數值,要麼就是長度比較短的字符串,Redis就會使用壓縮列表來做列表鍵的底層實現。

壓縮列表構成

壓縮列表是Redis爲了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構。一個壓縮列表可以包含任意多個接單,每個節點可以保存一個字節數組或者一個整數值。

壓縮節點構成

previous_entry_length:記錄壓縮列表中前一個節點的長度,如果前一節點長度小於254字節,長度爲1字節。如果大於等於254字節,長度爲5字節,第一字節設置爲0xFE(十進制254),之後的4字節保存前一節點的長度。

連鎖更新

Redis將特殊情況下產生的連續多次空間擴展操作稱之爲連鎖更新。連鎖更新最壞情況下需要對壓縮列表執行N次空間重分配操作,而每次空間重分配最壞複雜度爲O(N),所以連鎖更新最壞複雜度爲O(N^{2})

真正造成性能問題的機率是很低的:

  1. 壓縮列表中恰好有多個連續的、長度介於250字節至253字節之間的節點,連鎖更新纔有可能被引發,實際中,這種情況並不多見。
  2. 其次,即使出現連鎖更新,只要被更新的節點數量不多,就不會對性能造成任何影響
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章