1. 程式人生 > 實用技巧 >Redis閱讀筆記-Redis字串內部結構

Redis閱讀筆記-Redis字串內部結構

Redis閱讀筆記-Redis字串內部結構

​ Redis中的字串是可以修改的字串,在記憶體中它是以位元組陣列的形式存在。我們知道C語言中的字串標準形式是以NULL作為結束符, 但是在Redis裡面字串不是這麼表示的。因為要獲取NULL結尾的字串的長度使用strlen標準庫函式, 這個函式的演算法複雜度是O(n),它需要對位元組陣列進行遍歷掃描, 作為單執行緒的Redis表示承受不起。

​ Redis的字串叫[SDS],也就是Simple Dynamic String(簡單動態字串)。它的結構是一個帶長度的位元組資訊陣列。

struct SDS<T> {
    //陣列容量
    T capacity;
    //陣列長度
    T len;
    //特殊標誌位
    byte flags;
    //陣列內容
    byte[] content;
}

​ 如程式碼所示, content裡儲存了真正的字串內容, 那麼capacity和len表示什麼呢?它有點類似Java語言的ArrayList結構, 需要比實際的內容常多多分配一些冗餘空間。 capacity表示所分配陣列的長度,len表示字串的實際長度。 前面提到字串是可以修改的字串, 它要支援append操作。 如果陣列沒有冗餘空間,那麼追加操作必然涉及到分配新陣列, 然後將舊內容複製過來, 在append新內容。 如果字串的長度非常長, 這樣的記憶體分配和複製開銷就會非常大。

​ 上面的SDS結構使用了範型T,為什麼不直接用int呢, 這是因為當字串比較短時, len和capacity可以使用byte和shot表示, Redis為了對記憶體做極致的優化, 不同長度的字串使用不同的結構體表示。

​ Redis規定字串的長度不得超過512M位元組。建立字串時len和capacity一樣長, 不會多分配冗餘空間,這是因為大多數場景下我們不會使用append操作來修改字串。

embstr VS raw

​ Redis的字串有兩種儲存方式, 在長度特別短時, 使用emb形式儲存(embeded), 當長度超過44時, 使用raw形式儲存。

​ 這兩種型別有什麼區別呢?為什麼分界線是44呢?

127.0.0.1:6379> set codehole abcdefghijklmnopqrstuvwxyz012345678912345678 
OK 
127.0.0.1:6379> debug object codehole 
Value at:0x7fec2de00370 refcount:1 encoding:embstr serializedlength:45 lru:5958906 lru_seconds_idle:1 
127.0.0.1:6379> set codehole abcdefghijklmnopqrstuvwxyz0123456789123456789 
OK 
127.0.0.1:6379> debug object codehole
Value at:0x7fec2dd0b750 refcount:1 encoding:raw serializedlength:46 lru:5958911 lru_seconds_idle:1 

​ 注意上面debug object輸出中有個encoding欄位,一個字元的差別, 儲存形式就發生了變化。這是為什麼呢?

​ 為了解釋這個現象, 我們首先來了解下Redis物件頭結構體, 所有的Redis物件都有下面的結構頭:

struct RedisObject {
    //4bits
    int4 type;
    //4bits
    int4 encoding;
    //24bits
    int24 lru;
    //4bytes
    int32 refcount;
    //8bytes, 64-bit system
    void *ptr;
}robj;

​ 不同的物件具有不同的型別type(4bit), 同一型別的type會有不同的儲存形象encoding(4bit),為了記錄物件的LRU資訊, 使用了24個bit來記錄LRU資訊。每隔物件都有個引用計數, 當引用計數為零時, 物件就會被銷燬, 記憶體被回收。ptr指標將指向物件內容(body)的具體儲存位置。這樣一個RedisObject物件頭需要佔據16個位元組的儲存空間。

​ 接著再看SDS結構體的大小, 在字串比較小時, SDS物件頭的大小是capacity+3,至少是3。意味著分配一個字串的最小空間佔用為19(16+3)個位元組。

struct SDS {
    //1byte
    int8 capacity;
    //1byte
    int8 len;
    //1byte
    int8 flags;
    //內聯陣列, 長度為capacity
    byte[] content;
}

​ 如圖所示, embstr儲存形式是這樣的一個儲存形式, 它將RedisObject物件頭和SDS物件連續存在一起, 使用malloc方法一次分配。而raw儲存形式不一樣, 它需要兩次malloc,兩個物件頭在儲存地址上一般是不連續的。

​ 而記憶體分配器jemalloc/tcmalloc等分配記憶體大小的單位都是2、4、8、16、32、64等, 為了能容納一個完整的embstr物件, jemalloc最少分配32個位元組的空間, 如果字串在稍微長一點,那就是64位元組的空間。如果總體超出了64位元組, Redis認為它是大字串,不在使用embstr形式儲存, 而該用raw形式。

​ 當記憶體分配器分配了64空間時, 那麼這個字串的最大長度可以是多少呢? 這個長度就是44。那為什麼是44呢?

​ 前面提到SDS結構體中的content中的字串是以位元組\0結尾的字串,之所以多出這樣一個位元組, 就是為了便於直接使用glibc的字串處理函式, 以及為了便於子劇場的除錯列印輸出。

​ 看上面的這張圖可以算出, 留給content的長度最多隻有45(64-19)位元組了。字串又是以\0結尾, 所以embstr最大能容納的字串長度就是44。

擴容策略

​ 字串在長度小於1M之前,擴容空間採用加倍策略, 也就是保留100%的冗餘空間。當長度超過1M後, 為了避免加倍後的冗餘空間過大而導致浪費, 每次擴容只會多分1M大小的冗餘空間。