Redis底層數據結構之string


我們都知道,Redis是由C語言編寫的。在C語言中,字符串標准形式是以空字符\0作為結束符的,但是Redis里面的字符串卻沒有直接沿用C語言的字符串。主要是因為C語言中獲取字符串長度可以調用strlen這個標准函數,這個函數的時間復雜度是O(N),由於Redis是單線程的,承受不了這個時間復雜度。

Redis中string的存儲方式

在上一篇文章中,我們介紹了RedisRedisObject的數據結構,如下所示:

typedef struct redisObject {
    // 對外的類型 string list set hash zset等 4bit
    unsigned type:4;
    // 底層存儲方式 4bit
    unsigned encoding:4;
    // LRU 時間 24bit
    unsigned lru:LRU_BITS; 
    // 引用計數  4byte
    int refcount;
    // 指向對象的指針  8byte
    void *ptr;
} robj;

對於不同的對象,Redis會使用不同的類型來存儲。對於同一種類型type會有不同的存儲形式encoding。對於string類型的字符串,其底層編碼方式共有三種,分別為intembstrraw

  • int:當存儲的字符串全是數字時,此時使用int方式來存儲;
  • embstr:當存儲的字符串長度小於44個字符時,此時使用embstr方式來存儲;
  • raw:當存儲的字符串長度大於44個字符時,此時使用raw方式來存儲;

使用object encoding key可以查看key對應的encoding類型,如下所示:

對於embstrraw這兩種encoding類型,其存儲方式還不太一樣。對於embstr類型,它將RedisObject對象頭和SDS對象在內存中地址是連在一起的,但對於raw類型,二者在內存地址不是連續的。

SDS

在介紹string類型的存儲類型時,我們說到,對於embstrraw兩種類型其存儲方式不一樣,但ptr指針最后都指向一個SDS的結構。那什么是SDS呢?Redis中的字符串稱之為Simple Dynamic String,簡稱為SDS。與普通C語言的原始字符串結構相比,sds多了一個sdshdr的頭部信息,sdshdr基本數據結構如下所示:

struct sdsshr<T>{
    T len;//數組長度
    T alloc;//數組容量
    unsigned  flags;//sdshdr類型
    char buf[];//數組內容
}

可以看出,SDS的結構有點類似於Java中的ArrayListbuf[]表示真正存儲的字符串內容,alloc表示所分配的數組的長度,len表示字符串的實際長度,並且由於len這個屬性的存在,Redis可以在O(1)的時間復雜度內獲取數組長度。

為了追求對於內存的極致優化,對於不同長度的字符串,Redis底層會采用不同的結構體來表示。在Redis中的sds.h源碼中存在着五種sdshdr,分別如下:

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

上面說了,Redis底層會根據字符串的長度來決定具體使用哪種類型的sdshdr。可以看出,sdshdr5明顯區別於其他四種結構,它一般只用於存儲長度不會變化,且長度小於32個字符的字符串。但現在一般都不再使用該結構,因為其結構沒有lenalloc這兩個屬性,不具備動態擴容操作,一旦預分配的內存空間使用完,就需要重新分配內存並完成數據的復制和遷移,類似於ArrayList的擴容操作,這種操作對性能的影響很大。

上面介紹sdshdr屬性的時候說過,flag這個屬性用於標識使用哪種sdshdr類型,flag的低三位標識當前sds的類型,分別如下所示:

#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4

同時,注意到在每個sdshdr的頭定義上都有一個attribute((packed)),這個是為了告訴gcc取消優化對齊,這樣,每個字段分配的內存地址就是緊緊排列在一起的Redis中字符串參數的傳遞直接使用char*指針,其實現原理在於,由於sdshdr內存分配禁止了優化對齊,所以sds[-1]指向的就是flags屬性的內存地址,而通過flags屬性又可以確定sdshdr的屬性,進而可以讀取頭部字段確定sds的相關屬性。

sds的邏輯圖如下所示:

sdshdr的優勢

相比較於C語言原始的字符串,sdshdr的具備一些優勢。

長度獲取

由於sdshdr中存在len這個屬性,所以可以在O(1)的時間復雜度下獲得長度;而傳統的C語言得使用strlen這個標准函數獲取,時間復雜度為O(N)

避免頻繁的內存分配

原始的C語言一直使用與長度匹配的內存,這樣在追加字符串導致字符串長度發生變化時,就必須進行內存的重新分配。內存重新分配涉及到復雜算法和系統調用,耗費性能和時間。對於Redis來說,它是單線程的,如果使用原始的字符串結構,勢必會引發頻繁的內存重分配,這個顯然是不合理的。

因而,sds每次進行內存分配時,都會通過內存的預分配來減少因為修改字符串而引發的內存重分配次數。這個原理可以參數Java中的ArrayList,一般在使用ArrayList時都會建議使用帶有容量的構造方式,這樣可以避免頻繁resize

對於SDS來說,當其使用append進行字符串追加時,程序會用 alloc-len 比較下剩下的空余內存是否足夠分配追加的內容,如果不夠自然觸發內存重分配,而如果剩余未使用內存空間足夠放下,那么將直接進行分配,無需內存重分配。其擴容策略為,當字符串占用大小小於1M時,每次分配為len * 2,也就是保留100%的冗余;大於1M后,為了避免浪費,只多分配1M的空間。

通過這種預分配策略, SDS 將連續增長 N 次字符串所需的內存重分配次數從必定 N 次降低為最多 N 次。

緩沖區溢出

緩沖區溢出是指當某個數據超過了處理程序限制的范圍時,程序出現的異常操作。原始的C語言中,是由編碼者自己來分配字符串的內存,當出現內存分配不足時就會發生緩存區溢出。而sds的修改函數在修改前會判斷內存,動態的分配內存,杜絕了緩沖區溢出的可能性。

二進制安全

對於原始的C語言字符串來說,它會通過判斷當前字符串中是否存在空字符\0來確定是否已經是字符串的結尾。因而在某些情況下,如使用空格進行分割一段字符串時,或者是圖片或者視頻等二進制文件中存在\0等,就會出問題。而sds不是通過空字符串來判斷字符串是否已經到結尾,而是通過len這個字段的值。所以說,sds還具備二進制安全這個特性,即可以安全的存儲具有特殊格式的二進制數據。

總結

參考

https://juejin.im/post/5d7dac02518825297023fb35#comment

https://juejin.im/post/5dbf8e66e51d456ebe562ec6#heading-2

https://juejin.im/post/5ecb5eb96fb9a0480659da6a#heading-8


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM