Redis的底層實現---字串章節

布瑋發表於2018-08-28

Redis字串鍵的底層原理


不要遺忘最初的目標。 –RuiDer


本篇文章來源:《Redis設計與實現》一書,特別推薦

Category:

before

  • C語言基礎
  • Redis基礎

匯入

redis的命令如下:

            set x "hello";
            get x;
            hello

Redis作為一種儲存字串的快取結構,其具體實現是由C語言完成,在C語言中,字串是通過字元陣列實現的,即char[],那麼Redis對於字串的實現是不是也是基於字元陣列嗎?不是的,Redis對字串的處理是通過SDS(Simple Dynamic String)實現的。

SDS介紹

SDS(Simple Dynamic String)簡單動態字串,它是由C語言完成,如下是其具體實現

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

看看redis的示例:
    sdshdr
    free 0
    length 5
    buf     -->|'R'|'e'|'d'|'i'|'s'|'\0'|
解釋:
        - free0,表示這個SDS沒有分配任何未使用的空間
        - length為5,表示這個SDS儲存了一個長度為5的字串 
        - buf陣列中儲存著“Redis”字串

SDS遵循C字串以空字串結尾的慣例,儲存空字串的1位元組空間不計算在SDS的len屬性之中。

再看看SDS的free不為0的情況:

    sdshdr
    free 3
    length 5
    buf      -->|'R'|'e'|'d'|'i'|'s'| | | |

free的值為3,表示這個SDS分配了三個空閒的空間

SDS與字串的區別

C語言使用簡單的字串表示方式,並不能滿足Redis對字串在安全性,效率,以及功能方面的要求,SDS更使用Redis。

@1 常數複雜度獲取字串長度

C字串:
因為C語言並不記錄自身的長度資訊,所以獲取一個C字串的長度,程式必須遍歷整個字串,對遇到的,每個字元進行計數,直到遇到代表字串結尾的空字串為止,這個操作的複雜度為O(n)。

SDS:
與C語言不同的是,SDS結構中的屬性length記錄了SDS本身的長度,所以獲取一個SDS長度的複雜度為O(1)。有人疑問那麼SDS的length值是哪來的?這裡的length值是SDS API在設定和更新SDS時自動完成的。

總結1:通過使用SDS而不是C字串,Redis獲取字串長度的複雜度由O(N)降為O(1),這確保了字串長度的獲取的工作不會成為Redis的效能瓶頸。

@ 2杜絕緩衝區溢位

C字串:
由於C自身不記錄字串的長度帶來一個問題是容易造成緩衝區溢位(buffer overflow)。在<string.h>/strcat函式中,可以將一個字串拼接到另外一個字串的末尾。
char *strcat(char *dest,const char *src)
理想狀態下,使用者在使用這個函式時,假定C為dest分配了足夠多的記憶體,可以容納src字串中的所有內容,而一旦這個假定不成立,就會產生緩衝區溢位。舉個例子,假定記憶體中有相鄰的兩個字串s1,s2,如圖:

    s1                        s2
     |                         |
...|'R'|'e'|'d'|'i'|'s'|'\0'||'g'|'o'|'o'|'d'|'\0'|...

如果執行strcat(s1," cluster");將Redis改為”Redis cluster“,但是粗心的卻忘了在執行這句之前為s1分配足夠的空間,那麼在執行之後,s1的資料將會溢位到s2所在的空間,導致s2儲存的內容意外的被修改。

SDS:
與C語言不同的是,SDS空間分配政策完全杜絕了發生緩衝區溢位的可能性:當SDS API需要對字串進行修改時,首先會檢查SDS的空間是否滿足修改所需的要求,因為SDS自身有對字串長度記錄的屬性length和空閒空間屬性free,可以藉助這兩個引數進行檢查。SDS會在執行動作之前判斷SDS的空間大小,再去執行操作,如果空間不夠的話,SDS API會自動擴充套件空間。

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

C字串:
因為C字串不記錄自身長度,每次增長或者縮短字串長度時,程式都要對這個C字串陣列進行一次記憶體重新分配操作,不然容易造成記憶體益出。因為記憶體,分配設計複雜的演算法,並且可能需要執行系統呼叫,所以它通常是一個比較耗時和耗能的操作。但是Redis作為快取,追求速度,所以不能經常發生記憶體分配操作。

SDS:
SDS陣列中的未使用空間位元組數量由SDS的屬性free記錄,通過free記錄,SDS實現了空間預分配和惰性釋放兩種優化策略。
1. 空間預分配
空間預分配用於優化SDS的字串增長操作:當SDS的API對一個SDS進行修改,並且需要對SDS的空間進行擴充套件時,程式不僅會為SDS分配修改所需要的空間,而且還會為SDS分配額外的空間。額外的空間分配規則如下:
(1)如果修改SDS之後,SDS的長度小於1MB,那麼程式會給SDS分配和length一樣大的額外空間,這是SDSlength和free的值相等。舉個例子,如果修改後的字串長度為13k,那麼SDS的空間將會佔據13+13+1=27k(額外的一個位元組用於儲存空字串)。
(2)如果修改SDS之後,SDS的長度大於1MB,那麼程式會給SDS分配額外的1MB空間,舉個例子,比如修改後的SDS有30MB的大小,那麼程式會分配1MB的未使用空間,SDS的buf陣列實際大小將是30MB+1MB+1byte。
2.惰性釋放
惰性釋放用於優化SDS的字串縮短操作:當SDS的API要縮短SDS儲存的字串時,程式並不需要立即使用記憶體重分配策略來回收縮短後多出來的位元組,而是使用free屬性將這些位元組記錄起來,並等待使用。

@4 二進位制安全

C字串中的字元必須符合某種編碼(比如ASCII),並且除了字串末尾之外,字串裡面不能包含空字串,
否則最先被程式讀入的空字串將被誤認為是字串結尾。
SDS API都是二進位制安全的,所有SDS API都會以處理二進位制的方式來處理存放在SDS buf中的資料,資料寫什麼樣,它被讀取時就是什麼樣子。

@5 相容部分C字串函式

SDS的API總會以SDS儲存的資料的末尾設定為空字串,並且在分配SDS空間時會多分配一個位元組的空間來容納空字串,這是為了那些儲存的資料可以重用一部分<string.h>庫中的函式。

總結

字串和SDS之間的區別總結如下:

C字串 SDS
C字串獲取長度複雜度O(n) SDS獲取字串長度複雜度O(1)
API不是安全的,會出現緩衝區溢位 API是安全的,不會出現緩衝區溢位
修改字串長度N次必然執行N此記憶體重分配 修改字串長度N次必然執行最多N此記憶體重分配
只儲存文字資料 可以儲存文字資料或者二進位制資料
可以使用<string.h>庫中的函式 可以使用一部分

About Me

歡迎交流
我的Github

個人部落格

相關文章