1. 程式人生 > 其它 >《Redis設計與實現》(一)資料結構

《Redis設計與實現》(一)資料結構

一. 簡單動態字串(SDA,simple dynamic string)

1. 定義

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

SDS遵循C字串以空字串結尾的慣例,儲存空字元'\0'表示結尾,並不計算在len屬性中,而C語言則會在長度屬性計入最後一個空字串'\0';

2. SDS和C字串的區別

在Redis中,C字串只會作為字串字面量用在一些無需對字串值進行修改的地方;而SDS用途更廣泛;

i. 常量複雜度獲取字串長度

c字串獲取長度時間複雜度為O(N);因為有len欄位,SDS為O(1)。

ii. 杜絕快取區溢位

當SDS API需要對SDS進行修改時,API會先檢查SDS的空間是否滿足修改所需的要求,如果不滿足的話,API會自動將SDS的空間擴充套件至執行修改所需的大小,然後才執行實際的修改操作,不需要手動修改。

iii. 減少修改字串時帶來的記憶體重分配次數

C字串在每次進行拼接和截斷操作時,都需要對記憶體進行重新分配和釋放;SDS如果這樣做會對效能產生影響;

  1. 空間預分配,當需要分配空間(分配之後為p)時,會額外預分配一些空間(t);
    分配策略為:p < 1MB, t = len; p >= 1MB, t= 1MB; (空間還要加1Byte'\0')
  2. 惰性空間釋放,當SDS的API需要縮短SDS儲存的字串時,程式並不會立即使用記憶體重分配來回收縮短後多出來的位元組,而是使用free屬性將這些位元組的數量記錄起來,並等待將來使用。

iv. 二進位制安全

SDS的API都是使用處理二進位制的方式來處理SDS存放在buf數組裡的資料,不會出現讀到'\0'提前結束字串等現象的出現,所以SDS不僅可以儲存文字資料,還可以儲存任意格式的二進位制資料。

v. 相容部分C字串函式

可以使用一部分<string.h>庫中的函式。

二. 連結串列

1. 定義

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;

2. 特性

雙向、無環、帶連結串列長度和多型(用void *指標來儲存節點值);

三. 字典

1. 定義

//雜湊表
typedef struct dictht{
  dictEntry **table;  // 雜湊表陣列
  unsigned long size;    // 雜湊表大小
  unsigned long sizemask;  // 雜湊表大小掩碼,計算索引用,為size-1
  unsigned long used;    // 表中已有節點數量
}dictht;

// 雜湊表節點
typedef struct dictEntry {
  void *key;   // 鍵
  union{
    void *val;
    uint64_t u64;
    int64_t s64;
  } v;       // 值
  struct dictEntry *next;  // 指向下一個節點,形成連結串列(用於解決衝突)
};

typedef struct dict{
  dictType *type; // 型別特定函式
  void *privdata; // 包含有一些特定的私有函式,用於計算雜湊值、複製鍵、複製值、對比鍵、銷燬鍵和銷燬值
  dictht ht[2];    // 雜湊表,一個用來存資料,一個用來rehash
  int trehashidx;   // rehash索引
}dict; 

2. hash演算法

hash = dict->type->hashFunction(key);
index = hash &dict->ht[x].sizemask;

redis使用MurmurHash2演算法;

3. 解決衝突

新增到next上。

4. rehash

通過雜湊表的負載因子決定是否需要rehash;

負載因子: load_factor = ht[0].used / ht[0].size

未收到BGSAVE或BGREWRITEAOF時load_factor > 1, 收到時load_factor > 5; 達到上述條件,進行擴充套件;擴充套件至第一個大於等於ht[0].used*2的2^n;

load_factor < 0.1,進行收縮,收縮為第一個大於等於ht[0].used的2^n。

先擴充套件,再在ht[1]上進行rehash,再將ht[0]釋放,將ht[1]設定為ht[0],在ht[1]上放一個空表。

5. 漸進式rehash

用rehashidx記錄當前rehash的位置,在每次新增、刪除、查詢或更新時,同時附帶將ht[0]中rehashidx的值更新到ht[1]中,並將rehashidx++,並且rehash期間的新增、刪除、查詢或更新實在ht的兩個表上進行的,一個表上未找到還需要找另一個表。當所有鍵值對rehash後,rehashidx設為-1,rehash結束。

四. 跳錶

與二分查詢類似,跳躍表能夠在 O(㏒n)的時間複雜度之下完成查詢,與紅黑樹等資料結構查詢的時間複雜度相同,但是相比之下,跳躍表能夠更好的支援併發操作,而且實現這樣的結構比紅黑樹等資料結構要簡單、直觀許多。
跳躍表以有序的方式在層次化的連結串列中儲存元素,效率和平衡樹媲美:查詢、刪除、新增等操作都可以在對數期望時間下完成。跳躍表體現了“空間換時間”的思想,從本質上來說,跳躍表是在單鏈表的基礎上在選取部分結點新增索引,這些索引在邏輯關係上構成了一個新的線性表,並且索引的層數可以疊加,生成二級索引、三級索引、多級索引,以實現對結點的跳躍查詢的功能。
與二分查詢類似,跳躍表能夠在 O(㏒n)的時間複雜度之下完成查詢,與紅黑樹等資料結構查詢的時間複雜度相同,但是相比之下,跳躍表能夠更好的支援併發操作,而且實現這樣的結構比紅黑樹等資料結構要簡單、直觀許多。

五. 整數集合