Redis进阶-string底层数据结构精讲
文章目錄
- Pre
- string 字符串
- 字符串的實現
- 字符串 內部結構
- embstr vs raw
Pre
Redis進階-核心數據結構進階實戰
Redis 有 5 種基礎數據結構,分別為:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合) 。
Redis 所有的數據結構都是以唯一的key 字符串作為名稱,然后通過這個唯一 key 值來獲取相應的 value 數據。不同類型的數據結構的差異就在于 value 的結構不一樣。
string 字符串
字符串 string 是 Redis 最簡單的數據結構 .
舉個簡單的例子:緩存用戶信息。我們將用戶信息結構體使用 JSON 序列化成字符串,然后將序列化后的字符串塞進 Redis 來緩存。
同樣,取用戶信息會經過一次反序列化的過程。
當然了,不限于使用string存儲,看使用場景。
字符串的實現
Redis 的字符串是動態字符串,是可以修改的字符串,內部結構實現上類似于 Java 的ArrayList,采用預分配冗余空間的方式來減少內存的頻繁分配。
如上圖
-
內部為當前字符串實際分配的空間 capacity 一般要高于實際字符串長度 len.
-
當字符串長度小于 1M 時,擴容都是加倍現有的空間
-
超過 1M,擴容時一次只會多擴 1M 的空間
-
字符串最大長度為 512M
-
字符串是由多個字節組成,每個字節又是由 8 個 bit 組成,如此便可以將一個字符串看成很多 bit 的組合,這便是 bitmap「位圖」數據結構
字符串 內部結構
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 表示字符串的實際長度
前面我們提到字符串是可以修改的字符串,它要支持 append 操作。如果數組沒有冗余空間,那么追加操作必然涉及到分配新數組,然后將舊內容復制過來,再 append 新內容。如果字符串的長度非常長,這樣的內存分配和復制開銷就會非常大。
/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the * end of the specified sds string 's'. * * After the call, the passed sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. */ sds sdscatlen(sds s, const void *t, size_t len) {size_t curlen = sdslen(s); // 原字符串長度// 按需調整空間,如果 capacity 不夠容納追加的內容,就會重新分配字節數組并復制原字符串的內容到新數組中s = sdsMakeRoomFor(s,len);if (s == NULL) return NULL; // 內存不足memcpy(s+curlen, t, len); // 追加目標字符串的內容到字節數組中sdssetlen(s, curlen+len); // 設置追加后的長度值s[curlen+len] = '\0'; // 讓字符串以\0 結尾,便于調試打印,還可以直接使用 glibc 的字符串函數進行操作return s; }上面的 SDS 結構使用了范型 T,為什么不直接用 int 呢 ?
這是因為當字符串比較短時,len 和 capacity 可以使用 byte 和 short 來表示,Redis 為了對內存做極致的優化,不同長度的字符串使用不同的結構體來表示。
Redis 規定字符串的長度不得超過 512M 字節。創建字符串時 len 和 capacity 一樣長,不會多分配冗余空間,這是因為絕大多數場景下我們不會使用 append 操作來修改字符串。
embstr vs raw
Redis 的字符串有兩種存儲方式,在長度特別短時,使用 emb 形式存儲(embeded),當長度超過 44 時,使用 raw 形式存儲 。
上面 debug object 輸出中有個 encoding 字段,一個字符的差別,存儲形式就發生
了變化.
為啥呢? 我們首先來了解一下 Redis 對象頭結構體,所有的 Redis 對象都有
下面的這個結構頭:
-
不同的對象具有不同的類型 type(4bit),
-
同一個類型的 type 會有不同的存儲形式encoding(4bit),
-
為了記錄對象的 LRU 信息,使用了 24 個 bit 來記錄 LRU 信息。
-
每個對象都有個引用計數,當引用計數為零時,對象就會被銷毀,內存被回收。
-
ptr 指針將指向對象內容 (body) 的具體存儲位置。
這樣一個 RedisObject 對象頭需要占據 16 字節( 4bit + 4bit + 24bit + 4bytes + 8bytes )的存儲空間。
接著我們再看 SDS 結構體的大小,在字符串比較小時,SDS 對象頭的大小是capacity+3,至少是 3。意味著分配一個字符串的最小空間占用為 19 字節 (16+3)。
struct SDS {int8 capacity; // 1byteint8 len; // 1byteint8 flags; // 1bytebyte[] content; // 內聯數組,長度為 capacity }如圖所示,embstr 存儲形式是這樣一種存儲形式,它將 RedisObject 對象頭和 SDS 對象連續存在一起,使用 malloc 方法一次分配。
而 raw 存儲形式不一樣,它需要兩次malloc,兩個對象頭在內存地址上一般是不連續的。
而內存分配器 jemalloc/tcmalloc 等分配內存大小的單位都是 2、4、8、16、32、64 等等,為了能容納一個完整的 embstr 對象,jemalloc 最少會分配 32 字節的空間,如果字符串再稍微長一點,那就是 64 字節的空間。
如果總體超出了 64 字節,Redis 認為它是一個大字符串,不再使用 emdstr 形式存儲,而該用 raw 形式。
當內存分配器分配了 64 空間時,那這個字符串的長度最大可以是多少呢?這個長度就是 44。那為什么是 44 呢?
SDS 結構體中的 content 中的字符串是以字節\0 結尾的字符串,之所以多出這樣一個字節,是為了便于直接使用 glibc 的字符串處理函數,以及為了便于字符串的調試打印輸出。
看上面這張圖可以算出,留給 content 的長度最多只有 45(64-19) 字節了。字符串又是以\0 結尾,所以 embstr 最大能容納的字符串長度就是 44。
總結
以上是生活随笔為你收集整理的Redis进阶-string底层数据结构精讲的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis进阶-List底层数据结构精讲
- 下一篇: Redis进阶-无所不知的info命令诊