1. 程式人生 > 實用技巧 >Redis資料結構之SDS

Redis資料結構之SDS

前言

好早以前就像研究一下Redis了,一直以前都沒有時間和機會,其實都是自己找的藉口而已。做技術的基礎要打牢,下面準備跟著黃建巨集老師的Redis的 設計與實現 一書,學習鞏固一下最基本的資料結構。

Redis作為一個純C語言寫成的高效能中介軟體,不像Java、Python等高階語言內建很多資料結構,研究其資料結構的寫法肯定會有著不小的收穫。

接下來我會根據我的進度整理學習筆記,做一個跟著Redis學資料結構的專題。

先從字串開始,Redis 內使用 SDS 處理字串

1、SDS說明

  • SDS 即simple dynamic string,簡單動態字串;
  • SDS基於C語言的字元陣列進行一層封裝,並提供操作API;

SDS 相對於char字元陣列的優勢在於:

  1. 獲取字串的長度的事件複雜度為O(1);
  2. API 安全,即通過API操作sds不會造成緩衝區溢位;
  3. 每次修改字串不一定需求進行記憶體分配,提高效能;
  4. 可以儲存文字和二進位制資料(使用長度表示結束),char陣列不能存二進位制(因為\0表示結束);

2、SDS

sds的宣告和實現位於redis原始碼目錄下 src/sds.h中,sds.c是API的實現

這兩個檔案並沒有引入因他庫的標頭檔案,sdshdr 就是用最基本的c語言寫的資料結構

sds的宣告如下:

typedef char *sds;//這裡的sds就是sdshdr的buf指標

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

sdshdr 內包含一個儲存字串的字元陣列,一個表示儲存的字串長度的變數,以及一個表示分配了記憶體但是沒有使用到的的字串長度

sds 指標指向建立的 sdshdr 內的字元陣列(從建立API中可以看出)

對比於C語言的字元陣列,sds 有幾種特性:

  1. 儲存了字串長度;
  2. 避免緩衝區溢位;
  3. 惰性空間釋放,減少重新分配記憶體;
  4. 二進位制安全;
  5. 相容C語言字串函式;

2.1、儲存了字串長度

sdshdr 內的 len 變數儲存了 buff 中儲存的字串的長度,不包括\0

但是 len 的值,不完全等於 strlen(buff),因為 buff 中可能儲存的是二進位制資料,即當儲存文字資料時,len 等於 strlen(buff),當儲存二進位制資料的時候,len 不等於 strlen(buff)

C語言的 strlen() 方法獲取字串長度需要遍歷一遍字串,知道 \0 位置,時間複雜度為 O(N),sdshdr 獲取字串長度直接使用 len 即可,時間複雜度為 O(1),這是一種典型的以空間換時間的做法

2.2、避免緩衝區溢位

sds 的API在就行字串的新增的時候,都會先檢查現有記憶體是否足夠,若不足夠則先分配記憶體。這樣就避免了因忘記分配記憶體而新增資料導致的緩衝區溢位的問題出現

2.3、惰性空間釋放

sds API 在新增資料的時候,會分配記憶體,但是縮減的時候並不會立馬釋放記憶體,因為頻繁地操作記憶體,會影響效率,sds 有專門的API用於釋放記憶體

sds 的空間預分配決定如下:

  1. len 長度小於1M,則分配記憶體之後,len 會等於 free,即會申請所需空間的兩倍,一份留作使用,一份預留使用;
  2. len 長度大於1M,則會額外多申請1M的空間預留,即free 為1M

2.4、二進位制安全

C語言的字元陣列只能儲存文字,因為解析的時候以 \0 表示結束,但是若是二進位制資料(其中可能包括\0),就會出現資料遺漏

sds 是以 len 長度作為判斷結束的依據,因此,其可以儲存二進位制資料,是二進位制安全的

2.5、相容C語言字串函式

從原始碼中可以看出,sds 指標指向 sdshdrbuf,如下圖所示:

sds 型別其實也就是 char*,因此它能相容一部分C語言的庫函式甚至 string 類的函式

3、部分原始碼解析

redis 的sds API 的引數/返回值都是 sds 型別,沒有 sdshdr 這種結構體型別的,因為要儘量相容C語言的庫函式,所以,一般使用 sds 型別操作

typedef char *sds;//這裡的sds就是sdshdr的buf指標

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

下面方法用於獲取sds字串長度

static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}

這裡這裡的 sizeof(struct sdshdr),這裡涉及到以前的只是盲區了,這個sizeof() 的返回值是8,具體為什麼是8,後面講解。

s 指標指向 bufs-8 指向該 s 所處的的 sdshdr 型別,即為 sdshdr 指標,然後返回其成員 len 即可

這裡有個問題,為什麼sizeof(struct sdshdr)結果會是8?

3.1、柔性陣列

這裡涉及到兩個C語言的知識點:

  1. 結構體中的元素記憶體對其;
  2. 結構體中的柔型陣列;

記憶體對其不消多說,主要談談柔性陣列

柔性陣列(flexible array member)又稱伸縮性陣列成員,這種陣列主要是為了結構體而產生的。因為開發時,偶爾需要在結構體中存放長度可變的陣列,一般情況下,咱們會定義一個數組指標,需要時,分配記憶體使用,這樣有個缺點就是,記憶體利用的效率很低,所以柔性陣列作用就像動態陣列一樣,可以在結構體中存放一個長度動態的字串

  • 柔性陣列在結構體中的使用從C99開始;

  • 柔性陣列對於編譯器來說,長度為0,不佔結構體記憶體;

  • 柔性陣列必須放在結構體的最後;

  • 柔型陣列不是陣列指標,它是一個偏移量;

所以,這樣就好理解了,sdshdr 的 sizeof() 值只需要計算 len 和 free 即可,所以結果為8