【Redis學習筆記】字符串底層數據結構SDS

二、簡單動態字符串

2.1 SDS 的定義

SDS 是【簡單動態字符串】,其結構如下:

2Wqpn.jpg

  • free 屬性值爲 0,表示這個 SDS 沒有分配任何未使用的空間。
  • len 屬性值爲 5,表示這個 SDS 保存了一個五字節長的字符串(’\0’不包括在內)。
  • buf 屬性是一個 char 類型的數組,保存數據,最後一個字節保存了空字符 ‘\0’。

2.2 SDS 與 C 字符串的區別

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

因爲 C 字符串並不記錄自身的長度信息,所以爲了獲取一個 C 字符串的長度,程序必須遍歷整個字符串,對遇到的每個字符串進行計數,直到遇到了代表字符串結尾的空字符爲止,這個操作的複雜度爲 O(N)。而 SDS 在 len 屬性中記錄了 SDS 本身的長度,所以獲取一個 SDS 長度的複雜度僅爲 O(1)。

通過使用 SDS 而不是 C 字符串,Redis 將獲取字符串長度所需的複雜度從 O(N) 降低到 O(1),這確保了獲取字符串長度的工作不會成爲 Redis 的性能瓶頸。

2.2.2 杜絕緩衝區溢出

除了獲取字符串長度的複雜度高之外,C 字符串不記錄自身長度帶來的另一個問題就是容易造成緩衝區移除。

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

與 C 字符串不同,SDS 的空間分配策略完全杜絕了發生緩衝區溢出的可能性;當 SDS API 需要對 SDS 進行修改時,API 會先檢查 SDS 的空間是否滿足修改所需的要求,如果不滿足,API 會自動跳過 SDS 的空間擴展至執行修改所需的大小,然後才執行實際的修改操作。

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

因爲 C 字符串並不記錄自身的長度,所以對於一個包含 N 個字符串的 C 字符串來說,這個 C 字符串的底層實現總是一個 N+1 個字符長的數組(額外的一個字符空間用於保存空字符)。因爲 C 字符串的長度和底層數組的長度之間存在着這種關聯性,所以每次增長或縮短一個 C 字符串,程序都總要對保存這個 C 字符串的數組進行一次內存重分配操作:

  • 如果程序執行的是增長字符串的操作,比如拼接操作,那麼在執行這個操作之前,程序需要先通過內存分配來擴展底層數組的空間大小– 如果忘了這一點就會產生緩衝區溢出。
  • 如果程序執行的是縮短字符串的操作,比如截斷操作,那麼在執行這個操作之後,程序需要通過內存重分配來釋放字符串不再使用的那部分空間 – 如果忘了這一步就會產生內存泄漏。

因爲內存重分配涉及複雜的算法,並且可能需要執行系統調用,所以他是一個比較耗時的操作。

  • 在一般程序中,如果修改字符串長度的情況不太常出現,那麼每次修改都執行一次內存重分配是可以接受的。
  • 但是 Redis 作爲數據庫,經常被用於速度要求嚴苛、數據被頻繁修改的場合,如果每次修改字符串的長度都需要執行一次內存重分配的話,那麼光是執行內存重分配的時間就會佔去修改字符串所用時間的一大部分,如果這種修改頻繁發生的話,可能還會對性能造成影響。

爲了避免 C 字符串的這種缺陷,SDS 通過未使用空間解除了字符串長度和底層數組長度之間的關聯;在 SDS 中,buf 數組的長度不一定就是字符串數量加一,數組裏面可以包含1未使用的字節,而這些字節的數量就由 SDS 的 free 屬性記錄。

通過未使用空間,SDS 實現了空間預分配和惰性空間釋放兩種優化策略。

1. 空間預分配

空間預分配用於優化 SDS 的字符串增長操作,當 SDS 的 API 對一個 SDS 進行修改,並且需要對 SDS 進行空間拓展的時候,程序不僅會爲 SDS 分配修改所需要的空間,還會爲 SDS 分配額外的未使用空間。

其中,額外分配的未使用空間數量由以下公式決定:

  • 如果對 SDS 進行修改之後,SDS 的長度將小於 1MB,那麼程序分配和 len 屬性同樣大小的未使用空間,這時 SDS len 的屬性的值將和 free 屬性的值相同。
  • 如果對 SDS 進行修改之後,SDS 的長度將大於 1MB,那麼程序會分配 1MB 的未使用空間。

通過空間預分配策略,Redis 可以減少連續執行字符串增長操作所需的內存重分配次數。

在擴展 SDS 空間之前,SDS API 會先檢查未使用空間是否足夠,如果足夠的話,API 就會直接使用未使用空間,而無需執行內存重分配。

2. 惰性空間釋放

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

2.2.4 二進制安全

C 字符串中的字符必須符合某種編碼,並且除了字符串的末尾之外,字符串裏面不能包含空字符,否則最先被程序讀入的空字符將被誤認爲是字符串結尾,這些限制使得 C 字符串只能保存文本數據,而不能保存像圖片、音頻、視頻、壓縮文件這樣的二進制數據。

爲了確保 Redis 可以適用於各種不同的使用場景,SDS 的 API 都是二進制安全的,所有 SDS API 都會以處理二進制的方式來處理 SDS 存放在 buf 數組裏的數據,程序不會對其中的數據做任何限制、過濾、或者假設,數據在寫入時是怎麼樣的,他被讀取時就是怎麼樣的。

通過使用二進制安全的 SDS,而不是使用 C 字符串,使得 Redis 不僅可以一保存文本數據,還可以保存任意格式的二進制數據。

2.2.5 兼容部分 C 字符串函數

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

總結:

C 字符串 SDS
獲取字符串長度的複雜度爲 O(N) 獲取字符串長度的複雜度爲 O(1)
API 是不安全的,可能會造成緩衝區溢出 API 是安全的,不會造成緩衝區溢出
修改字符串長度 N 次必然需要執行 N 次內存重分配 修改字符串長度 N 次最多需要執行 N 次內存重分配
只能保存文本數據 可以保存文本或者二進制數據
可以使用 <string.h> 庫中的函數 可以使用一部分 <string.h> 庫中的函數
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章