Redis进阶-string底层数据结构精讲
文章目錄
- Pre
- string 字符串
- 字符串的實(shí)現(xiàn)
- 字符串 內(nèi)部結(jié)構(gòu)
- embstr vs raw
Pre
Redis進(jìn)階-核心數(shù)據(jù)結(jié)構(gòu)進(jìn)階實(shí)戰(zhàn)
Redis 有 5 種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),分別為:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合) 。
Redis 所有的數(shù)據(jù)結(jié)構(gòu)都是以唯一的key 字符串作為名稱(chēng),然后通過(guò)這個(gè)唯一 key 值來(lái)獲取相應(yīng)的 value 數(shù)據(jù)。不同類(lèi)型的數(shù)據(jù)結(jié)構(gòu)的差異就在于 value 的結(jié)構(gòu)不一樣。
string 字符串
字符串 string 是 Redis 最簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu) .
舉個(gè)簡(jiǎn)單的例子:緩存用戶(hù)信息。我們將用戶(hù)信息結(jié)構(gòu)體使用 JSON 序列化成字符串,然后將序列化后的字符串塞進(jìn) Redis 來(lái)緩存。
同樣,取用戶(hù)信息會(huì)經(jīng)過(guò)一次反序列化的過(guò)程。
當(dāng)然了,不限于使用string存儲(chǔ),看使用場(chǎng)景。
字符串的實(shí)現(xiàn)
Redis 的字符串是動(dòng)態(tài)字符串,是可以修改的字符串,內(nèi)部結(jié)構(gòu)實(shí)現(xiàn)上類(lèi)似于 Java 的ArrayList,采用預(yù)分配冗余空間的方式來(lái)減少內(nèi)存的頻繁分配。
如上圖
-
內(nèi)部為當(dāng)前字符串實(shí)際分配的空間 capacity 一般要高于實(shí)際字符串長(zhǎng)度 len.
-
當(dāng)字符串長(zhǎng)度小于 1M 時(shí),擴(kuò)容都是加倍現(xiàn)有的空間
-
超過(guò) 1M,擴(kuò)容時(shí)一次只會(huì)多擴(kuò) 1M 的空間
-
字符串最大長(zhǎng)度為 512M
-
字符串是由多個(gè)字節(jié)組成,每個(gè)字節(jié)又是由 8 個(gè) bit 組成,如此便可以將一個(gè)字符串看成很多 bit 的組合,這便是 bitmap「位圖」數(shù)據(jù)結(jié)構(gòu)
字符串 內(nèi)部結(jié)構(gòu)
Redis 中的字符串是可以修改的字符串,在內(nèi)存中它是以字節(jié)數(shù)組的形式存在的。
C 語(yǔ)言里面的字符串標(biāo)準(zhǔn)形式是以 NULL 作為結(jié)束符,但是在 Redis 里面字符串不
是這么表示的。因?yàn)橐@取 NULL 結(jié)尾的字符串的長(zhǎng)度使用的是 strlen 標(biāo)準(zhǔn)庫(kù)函數(shù),這個(gè)函數(shù)的算法復(fù)雜度是 O(n),它需要對(duì)字節(jié)數(shù)組進(jìn)行遍歷掃描,作為單線(xiàn)程的 Redis 表示承受不起。
Redis 的字符串叫著「SDS」,也就是 Simple Dynamic String。它的結(jié)構(gòu)是一個(gè)帶長(zhǎng)度信息的字節(jié)數(shù)組。
struct SDS<T> {T capacity; // 數(shù)組容量 使用泛型表示的T len; // 數(shù)組長(zhǎng)度 使用泛型表示的 byte flags; // 特殊標(biāo)識(shí)位,不理睬它byte[] content; // 數(shù)組內(nèi)容 字節(jié)數(shù)組 }- content 里面存儲(chǔ)了真正的字符串內(nèi)容
- capacity 表示所分配數(shù)組的長(zhǎng)度
- len 表示字符串的實(shí)際長(zhǎng)度
前面我們提到字符串是可以修改的字符串,它要支持 append 操作。如果數(shù)組沒(méi)有冗余空間,那么追加操作必然涉及到分配新數(shù)組,然后將舊內(nèi)容復(fù)制過(guò)來(lái),再 append 新內(nèi)容。如果字符串的長(zhǎng)度非常長(zhǎng),這樣的內(nèi)存分配和復(fù)制開(kāi)銷(xiāo)就會(huì)非常大。
/* 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); // 原字符串長(zhǎng)度// 按需調(diào)整空間,如果 capacity 不夠容納追加的內(nèi)容,就會(huì)重新分配字節(jié)數(shù)組并復(fù)制原字符串的內(nèi)容到新數(shù)組中s = sdsMakeRoomFor(s,len);if (s == NULL) return NULL; // 內(nèi)存不足memcpy(s+curlen, t, len); // 追加目標(biāo)字符串的內(nèi)容到字節(jié)數(shù)組中sdssetlen(s, curlen+len); // 設(shè)置追加后的長(zhǎng)度值s[curlen+len] = '\0'; // 讓字符串以\0 結(jié)尾,便于調(diào)試打印,還可以直接使用 glibc 的字符串函數(shù)進(jìn)行操作return s; }上面的 SDS 結(jié)構(gòu)使用了范型 T,為什么不直接用 int 呢 ?
這是因?yàn)楫?dāng)字符串比較短時(shí),len 和 capacity 可以使用 byte 和 short 來(lái)表示,Redis 為了對(duì)內(nèi)存做極致的優(yōu)化,不同長(zhǎng)度的字符串使用不同的結(jié)構(gòu)體來(lái)表示。
Redis 規(guī)定字符串的長(zhǎng)度不得超過(guò) 512M 字節(jié)。創(chuàng)建字符串時(shí) len 和 capacity 一樣長(zhǎng),不會(huì)多分配冗余空間,這是因?yàn)榻^大多數(shù)場(chǎng)景下我們不會(huì)使用 append 操作來(lái)修改字符串。
embstr vs raw
Redis 的字符串有兩種存儲(chǔ)方式,在長(zhǎng)度特別短時(shí),使用 emb 形式存儲(chǔ)(embeded),當(dāng)長(zhǎng)度超過(guò) 44 時(shí),使用 raw 形式存儲(chǔ) 。
上面 debug object 輸出中有個(gè) encoding 字段,一個(gè)字符的差別,存儲(chǔ)形式就發(fā)生
了變化.
為啥呢? 我們首先來(lái)了解一下 Redis 對(duì)象頭結(jié)構(gòu)體,所有的 Redis 對(duì)象都有
下面的這個(gè)結(jié)構(gòu)頭:
-
不同的對(duì)象具有不同的類(lèi)型 type(4bit),
-
同一個(gè)類(lèi)型的 type 會(huì)有不同的存儲(chǔ)形式encoding(4bit),
-
為了記錄對(duì)象的 LRU 信息,使用了 24 個(gè) bit 來(lái)記錄 LRU 信息。
-
每個(gè)對(duì)象都有個(gè)引用計(jì)數(shù),當(dāng)引用計(jì)數(shù)為零時(shí),對(duì)象就會(huì)被銷(xiāo)毀,內(nèi)存被回收。
-
ptr 指針將指向?qū)ο髢?nèi)容 (body) 的具體存儲(chǔ)位置。
這樣一個(gè) RedisObject 對(duì)象頭需要占據(jù) 16 字節(jié)( 4bit + 4bit + 24bit + 4bytes + 8bytes )的存儲(chǔ)空間。
接著我們?cè)倏?SDS 結(jié)構(gòu)體的大小,在字符串比較小時(shí),SDS 對(duì)象頭的大小是capacity+3,至少是 3。意味著分配一個(gè)字符串的最小空間占用為 19 字節(jié) (16+3)。
struct SDS {int8 capacity; // 1byteint8 len; // 1byteint8 flags; // 1bytebyte[] content; // 內(nèi)聯(lián)數(shù)組,長(zhǎng)度為 capacity }如圖所示,embstr 存儲(chǔ)形式是這樣一種存儲(chǔ)形式,它將 RedisObject 對(duì)象頭和 SDS 對(duì)象連續(xù)存在一起,使用 malloc 方法一次分配。
而 raw 存儲(chǔ)形式不一樣,它需要兩次malloc,兩個(gè)對(duì)象頭在內(nèi)存地址上一般是不連續(xù)的。
而內(nèi)存分配器 jemalloc/tcmalloc 等分配內(nèi)存大小的單位都是 2、4、8、16、32、64 等等,為了能容納一個(gè)完整的 embstr 對(duì)象,jemalloc 最少會(huì)分配 32 字節(jié)的空間,如果字符串再稍微長(zhǎng)一點(diǎn),那就是 64 字節(jié)的空間。
如果總體超出了 64 字節(jié),Redis 認(rèn)為它是一個(gè)大字符串,不再使用 emdstr 形式存儲(chǔ),而該用 raw 形式。
當(dāng)內(nèi)存分配器分配了 64 空間時(shí),那這個(gè)字符串的長(zhǎng)度最大可以是多少呢?這個(gè)長(zhǎng)度就是 44。那為什么是 44 呢?
SDS 結(jié)構(gòu)體中的 content 中的字符串是以字節(jié)\0 結(jié)尾的字符串,之所以多出這樣一個(gè)字節(jié),是為了便于直接使用 glibc 的字符串處理函數(shù),以及為了便于字符串的調(diào)試打印輸出。
看上面這張圖可以算出,留給 content 的長(zhǎng)度最多只有 45(64-19) 字節(jié)了。字符串又是以\0 結(jié)尾,所以 embstr 最大能容納的字符串長(zhǎng)度就是 44。
總結(jié)
以上是生活随笔為你收集整理的Redis进阶-string底层数据结构精讲的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Redis进阶-List底层数据结构精讲
- 下一篇: Redis进阶-无所不知的info命令诊