Redis設計與實現------讀書筆記(第二章)

REDIS簡單動態字符串

redis中的健值名使用的是SDS這種數據對象,它與C語言原生的字符串不同.
在Redis裏面,C字符串只會作爲字符串字面量(string literal)用在一些無須對字符串值進行修改的地方,比如打印日誌:

redisLog(REDIS_WARNING,"Redis is now ready to exit, bye bye...");

但是C原生的字符串並不能滿足頻繁操作的redis
又比如,如果客戶端執行命令:

redis> RPUSH fruits "apple" "banana" "cherry"
(integer) 3

那麼Redis將在數據庫中創建一個新的鍵值對,其中:
● 鍵值對的鍵是一個字符串對象,對象的底層實現是一個保存了字符串"fruits"的SDS。
● 鍵值對的值是一個列表對象,列表對象包含了三個字符串對象,這三個字符串對象分別由三個SDS實現:第一個SDS保存着字符串"apple",第二個SDS保存着字符串"banana",第三個SDS保存着字符串"cherry"。

2.1 SDS的定義

struct sdshdr {
    // 記錄buf數組中已使用字節的數量
    // 等於SDS所保存字符串的長度
    int len;

    // 記錄buf數組中未使用字節的數量
    int free;

    // 字節數組,用於保存字符串
    char buf[];
};

SDS遵循C字符串以空字符結尾的慣例,保存空字符的1字節空間不計算在SDS的len屬性裏面,並且爲空字符分配額外的1字節空間,以及添加空字符到字符串末尾等操作,都是由SDS函數自動完成的,所以這個空字符對於SDS的使用者來說是完全透明的。遵循空字符結尾這一慣例的好處是,SDS可以直接重用一部分C字符串函數庫裏面的函數。

2.2 SDS與C字符串的區別

2.2.1 常數複雜度獲取字符串長度

C語言使用長度爲N+1的字符數組來表示長度爲N的字符串,並且字符數組的最後一個元素總是空字符’\0’。
因爲C字符串並不記錄自身的長度信息,所以爲了獲取一個C字符串的長度,程序必須遍歷整個字符串,對遇到的每個字符進行計數,直到遇到代表字符串結尾的空字符爲止,這個操作的複雜度爲O(N)
和C字符串不同,因爲SDS在len屬性中記錄了SDS本身的長度,所以獲取一個SDS長度的複雜度僅爲O(1)。
總結:利用空間換時間,記錄字符串長度,下次獲取直接通過屬性來獲取而不用遍歷字符串取得

設置和更新SDS長度的工作是由SDS的API在執行時自動完成的,使用SDS無須進行任何手動修改長度的工作

2.2.2 杜絕緩衝區溢出

C字符串不記錄自身長度帶來的另一個問題是容易造成緩衝區溢出(buffer Overflow)。舉個例子,<string.h>/strcat函數可以將src字符串中的內容拼接到dest字符串的末尾:

char *strcat(char *dest, const char *src);

因爲C字符串不記錄自身的長度,所以strcat假定用戶在執行這個函數時,已經爲dest分配了足夠多的內存,可以容納src字符串中的所有內容,而一旦這個假定不成立時,就會產生緩衝區溢出。

與C字符串不同,SDS的空間分配策略完全杜絕了發生緩衝區溢出的可能性:當SDS API需要對SDS進行修改時,API會先檢查SDS的空間是否滿足修改所需的要求,如果不滿足的話,API會自動將SDS的空間擴展至執行修改所需的大小,然後才執行實際的修改操作,所以使用SDS既不需要手動修改SDS的空間大小,也不會出現前面所說的緩衝區溢出問題。

2.2.3 減少修改字符串時帶來的內存重分配次數

因爲C字符串的長度和底層數組的長度之間存在着這種關聯性,所以每次增長或者縮短一個C字符串,程序都總要對保存這個C字符串的數組進行一次內存重分配操作:
● 如果程序執行的是增長字符串的操作,比如拼接操作(append),那麼在執行這個操作之前,程序需要先通過內存重分配來擴展底層數組的空間大小——如果忘了這一步就會產生緩衝區溢出。
● 如果程序執行的是縮短字符串的操作,比如截斷操作(trim),那麼在執行這個操作之後,程序需要通過內存重分配來釋放字符串不再使用的那部分空間——如果忘了這一步就會產生內存泄漏
爲了避免C字符串的這種缺陷,SDS通過未使用空間解除了字符串長度和底層數組長度之間的關聯:在SDS中,buf數組的長度不一定就是字符數量加一,數組裏面可以包含未使用的字節,而這些字節的數量就由SDS的free屬性記錄。
通過未使用空間,SDS實現了空間預分配和惰性空間釋放兩種優化策略。

1空間預分配

空間預分配用於優化SDS的字符串增長操作:當SDS的API對一個SDS進行修改,並且需要對SDS進行空間擴展的時候,程序不僅會爲SDS分配修改所必須要的空間,還會爲SDS分配額外的未使用空間。
其中,額外分配的未使用空間數量由以下公式決定:
如果對SDS進行修改之後,SDS的長度將大於等於1MB,那麼程序會分配1MB的未使用空間。舉個例子,如果進行修改之後,SDS的len將變成30MB,那麼程序會分配1MB的未使用空間,SDS的buf數組的實際長度將爲30 MB + 1MB + 1byte。
通過空間預分配策略,Redis可以減少連續執行字符串增長操作所需的內存重分配次數。
● 如果對SDS進行修改之後,SDS的長度將大於等於1MB,那麼程序會分配1MB的未使用空間。舉個例子,如果進行修改之後,SDS的len將變成30MB,那麼程序會分配1MB的未使用空間,SDS的buf數組的實際長度將爲30 MB + 1MB + 1byte。
通過空間預分配策略,Redis可以減少連續執行字符串增長操作所需的內存重分配次數。

2惰性空間釋放

惰性空間釋放用於優化SDS的字符串縮短操作:當SDS的API需要縮短SDS保存的字符串時,程序並不立即使用內存重分配來回收縮短後多出來的字節,而是使用free屬性將這些字節的數量記錄起來,並等待將來使用。

2.2.4 二進制安全

C字符串中的字符必須符合某種編碼(比如ASCII),並且除了字符串的末尾之外,字符串裏面不能包含空字符,否則最先被程序讀入的空字符將被誤認爲是字符串結尾,這些限制使得C字符串只能保存文本數據,而不能保存像圖片、音頻、視頻、壓縮文件這樣的二進制數據。
使用SDS來保存之前提到的特殊數據格式就沒有任何問題,因爲SDS使用len屬性的值而不是空字符來判斷字符串是否結束

2.2.5 兼容部分C字符串函數

然SDS的API都是二進制安全的,但它們一樣遵循C字符串以空字符結尾的慣例:這些API總會將SDS保存的數據的末尾設置爲空字符,並且總會在爲buf數組分配空間時多分配一個字節來容納這個空字符,這是爲了讓那些保存文本數據的SDS可以重用一部分<string.h>庫定義的函數。

2.2.6 總結

以下是C字符串與Redis的SDS數據將對象之間的區別
在這裏插入圖片描述

2.3 SDS 的 API

在這裏插入圖片描述
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章