自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Redis中萬金油的String,為什么不好用了?

開發(fā) 前端
在這篇文章中,我們將顛覆以往對 String 數(shù)據(jù)類型的傳統(tǒng)認知。以前,String 被視為一種“萬金油”,在各種場合都被廣泛使用。然而,當(dāng)存儲的鍵值對數(shù)據(jù)本身占用的內(nèi)存空間較小時,String 類型的元數(shù)據(jù)開銷占據(jù)了主導(dǎo)地位。

今天,我們先了解下 String 類型的內(nèi)存空間消耗問題,以及選擇節(jié)省內(nèi)存開銷的數(shù)據(jù)類型的解決方案。

我想和你分享一個之前我面臨的需求案例。

曾經(jīng),我們面臨著一個任務(wù),要創(chuàng)建一個高效的圖片存儲系統(tǒng),要求這個系統(tǒng)能夠快速記錄圖片 ID 和圖片在存儲系統(tǒng)中的唯一標(biāo)識(我們稱之為圖片存儲對象 ID)。此外,還需要能夠通過圖片 ID 快速檢索到相應(yīng)的圖片存儲對象 ID。

考慮到圖片數(shù)量龐大,我們決定使用 10 位數(shù)字來表示圖片 ID 和圖片存儲對象 ID。舉個例子,圖片 ID 可能是 1101000051,對應(yīng)的存儲對象 ID 則是 3301000051。

photo_id: 1101000051

photo_obj_id: 3301000051

這個案例很明顯地展現(xiàn)了“鍵 - 單值”模式。在這種模式中,每個鍵值對中的值都是一個單一的值,而不是一個值的集合,與 String 類型的數(shù)據(jù)存儲方式完美契合。

另外,String 類型的數(shù)據(jù)可以保存二進制字節(jié)流,這使得它非常靈活,只需將數(shù)據(jù)轉(zhuǎn)換成二進制字節(jié)數(shù)組,就可以輕松地進行存儲。

因此,我們的初始解決方案是使用 String 類型來存儲數(shù)據(jù)。我們將圖片 ID 和圖片存儲對象 ID 分別用作鍵值對中的鍵和值,其中圖片存儲對象 ID 使用了 String 類型。

最初,我們成功地存儲了一億張圖片,大約使用了 6.4GB 的內(nèi)存。但是,隨著圖片數(shù)據(jù)不斷增加,我們開始遇到了問題,Redis 實例的內(nèi)存使用量不斷上升,導(dǎo)致生成 RDB 文件時出現(xiàn)延遲的情況。顯然,String 類型并不是一個適合大規(guī)模數(shù)據(jù)存儲的理想選擇,因此我們需要尋找更為節(jié)省內(nèi)存開銷的數(shù)據(jù)類型解決方案。

在這個過程中,我深入研究了 String 類型的底層結(jié)構(gòu),找出了它內(nèi)存開銷較大的原因。這讓我對這個“通用型”的 String 數(shù)據(jù)類型有了新的認識,它并不適用于所有情況,尤其在內(nèi)存空間消耗方面存在明顯短板。

與此同時,我還仔細研究了集合類型的數(shù)據(jù)結(jié)構(gòu),發(fā)現(xiàn)它們具有非常高效的內(nèi)存管理結(jié)構(gòu)。但是,集合類型的數(shù)據(jù)結(jié)構(gòu)通常用于保存一鍵多值的數(shù)據(jù),不太適用于直接存儲單一鍵對應(yīng)的單一值。因此,我們采用了二級編碼的方法,成功地使用集合類型來存儲單一鍵值對。這種改變顯著降低了 Redis 實例的內(nèi)存開銷。

在本篇文章中,我將與你分享我在解決這一問題過程中所獲得的經(jīng)驗和方法,包括 String 類型的內(nèi)存開銷問題,可節(jié)省內(nèi)存的數(shù)據(jù)結(jié)構(gòu)選擇,以及如何使用集合類型來存儲單一鍵值對。如果你在使用 String 類型時也遇到了內(nèi)存開銷較大的問題,那么今天的解決方案可能會對你有所幫助。

接下來,我們先來看看 String 類型的內(nèi)存都消耗在哪里了。

為什么 String 類型內(nèi)存開銷大?

在剛才的案例中,我們保存了 1 億張圖片的信息,用了約 6.4GB 的內(nèi)存,一個圖片 ID 和圖片存儲對象 ID 的記錄平均用了 64 字節(jié)。

但問題是,一組圖片 ID 及其存儲對象 ID 的記錄,實際只需要 16 字節(jié)就可以了。

我們來分析一下。圖片 ID 和圖片存儲對象 ID 都是 10 位數(shù),我們可以用兩個 8 字節(jié)的 Long 類型表示這兩個 ID。因為 8 字節(jié)的 Long 類型最大可以表示 2 的 64 次方的數(shù)值,所以肯定可以表示 10 位數(shù)。但是,為什么 String 類型卻用了 64 字節(jié)呢?

其實,除了記錄實際數(shù)據(jù),String 類型還需要額外的內(nèi)存空間記錄數(shù)據(jù)長度、空間使用等信息,這些信息也叫作元數(shù)據(jù)。當(dāng)實際保存的數(shù)據(jù)較小時,元數(shù)據(jù)的空間開銷就顯得比較大了,有點“喧賓奪主”的意思。

那么,String 類型具體是怎么保存數(shù)據(jù)的呢?我來解釋一下。

當(dāng)你保存 64 位有符號整數(shù)時,String 類型會把它保存為一個 8 字節(jié)的 Long 類型整數(shù),這種保存方式通常也叫作 int 編碼方式。

但是,當(dāng)你保存的數(shù)據(jù)中包含字符時,String 類型就會用簡單動態(tài)字符串(Simple Dynamic String,SDS)結(jié)構(gòu)體來保存,如下圖所示:

圖片圖片

buf:字節(jié)數(shù)組,保存實際數(shù)據(jù)。為了表示字節(jié)數(shù)組的結(jié)束,Redis 會自動在數(shù)組最后加一個“\0”,這就會額外占用 1 個字節(jié)的開銷。

len:占 4 個字節(jié),表示 buf 的已用長度。

alloc:也占個 4 字節(jié),表示 buf 的實際分配長度,一般大于 len。

可以看到,在 SDS 中,buf 保存實際數(shù)據(jù),而 len 和 alloc 本身其實是 SDS 結(jié)構(gòu)體的額外開銷。

另外,對于 String 類型來說,除了 SDS 的額外開銷,還有一個來自于 RedisObject 結(jié)構(gòu)體的開銷。

因為 Redis 的數(shù)據(jù)類型有很多,而且,不同數(shù)據(jù)類型都有些相同的元數(shù)據(jù)要記錄(比如最后一次訪問的時間、被引用的次數(shù)等),所以,Redis 會用一個 RedisObject 結(jié)構(gòu)體來統(tǒng)一記錄這些元數(shù)據(jù),同時指向?qū)嶋H數(shù)據(jù)。

一個 RedisObject 包含了 8 字節(jié)的元數(shù)據(jù)和一個 8 字節(jié)指針,這個指針再進一步指向具體數(shù)據(jù)類型的實際數(shù)據(jù)所在,例如指向 String 類型的 SDS 結(jié)構(gòu)所在的內(nèi)存地址,可以看一下下面的示意圖。關(guān)于 RedisObject 的具體結(jié)構(gòu)細節(jié),我會在后面的課程中詳細介紹,現(xiàn)在你只要了解它的基本結(jié)構(gòu)和元數(shù)據(jù)開銷就行了。

圖片圖片

為了節(jié)省內(nèi)存空間,Redis 還對 Long 類型整數(shù)和 SDS 的內(nèi)存布局做了專門的設(shè)計。

一方面,當(dāng)保存的是 Long 類型整數(shù)時,RedisObject 中的指針就直接賦值為整數(shù)數(shù)據(jù)了,這樣就不用額外的指針再指向整數(shù)了,節(jié)省了指針的空間開銷。

另一方面,當(dāng)保存的是字符串?dāng)?shù)據(jù),并且字符串小于等于 44 字節(jié)時,RedisObject 中的元數(shù)據(jù)、指針和 SDS 是一塊連續(xù)的內(nèi)存區(qū)域,這樣就可以避免內(nèi)存碎片。這種布局方式也被稱為 embstr 編碼方式。

當(dāng)然,當(dāng)字符串大于 44 字節(jié)時,SDS 的數(shù)據(jù)量就開始變多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是會給 SDS 分配獨立的空間,并用指針指向 SDS 結(jié)構(gòu)。這種布局方式被稱為 raw 編碼模式。

為了幫助你理解 int、embstr 和 raw 這三種編碼模式,我畫了一張示意圖,如下所示:

圖片圖片

好了,知道了 RedisObject 所包含的額外元數(shù)據(jù)開銷,現(xiàn)在,我們就可以計算 String 類型的內(nèi)存使用量了。

因為 10 位數(shù)的圖片 ID 和圖片存儲對象 ID 是 Long 類型整數(shù),所以可以直接用 int 編碼的 RedisObject 保存。每個 int 編碼的 RedisObject 元數(shù)據(jù)部分占 8 字節(jié),指針部分被直接賦值為 8 字節(jié)的整數(shù)了。此時,每個 ID 會使用 16 字節(jié),加起來一共是 32 字節(jié)。但是,另外的 32 字節(jié)去哪兒了呢?

Redis 會使用一個全局哈希表保存所有鍵值對,哈希表的每一項是一個 dictEntry 的結(jié)構(gòu)體,用來指向一個鍵值對。dictEntry 結(jié)構(gòu)中有三個 8 字節(jié)的指針,分別指向 key、value 以及下一個 dictEntry,三個指針共 24 字節(jié),如下圖所示:

圖片圖片

但是,這三個指針只有 24 字節(jié),為什么會占用了 32 字節(jié)呢?這就要提到 Redis 使用的內(nèi)存分配庫 jemalloc 了。

jemalloc 在分配內(nèi)存時,會根據(jù)我們申請的字節(jié)數(shù) N,找一個比 N 大,但是最接近 N 的 2 的冪次數(shù)作為分配的空間,這樣可以減少頻繁分配的次數(shù)。

舉個例子。如果你申請 6 字節(jié)空間,jemalloc 實際會分配 8 字節(jié)空間;如果你申請 24 字節(jié)空間,jemalloc 則會分配 32 字節(jié)。所以,在我們剛剛說的場景里,dictEntry 結(jié)構(gòu)就占用了 32 字節(jié)。

好了,到這兒,你應(yīng)該就能理解,為什么用 String 類型保存圖片 ID 和圖片存儲對象 ID 時需要用 64 個字節(jié)了。

你看,明明有效信息只有 16 字節(jié),使用 String 類型保存時,卻需要 64 字節(jié)的內(nèi)存空間,有 48 字節(jié)都沒有用于保存實際的數(shù)據(jù)。我們來換算下,如果要保存的圖片有 1 億張,那么 1 億條的圖片 ID 記錄就需要 6.4GB 內(nèi)存空間,其中有 4.8GB 的內(nèi)存空間都用來保存元數(shù)據(jù)了,額外的內(nèi)存空間開銷很大。那么,有沒有更加節(jié)省內(nèi)存的方法呢?

用什么數(shù)據(jù)結(jié)構(gòu)可以節(jié)省內(nèi)存?

Redis 有一種底層數(shù)據(jù)結(jié)構(gòu),叫壓縮列表(ziplist),這是一種非常節(jié)省內(nèi)存的結(jié)構(gòu)。

我們先回顧下壓縮列表的構(gòu)成。表頭有三個字段 zlbytes、zltail 和 zllen,分別表示列表長度、列表尾的偏移量,以及列表中的 entry 個數(shù)。壓縮列表尾還有一個 zlend,表示列表結(jié)束。

圖片圖片

壓縮列表之所以能節(jié)省內(nèi)存,就在于它是用一系列連續(xù)的 entry 保存數(shù)據(jù)。每個 entry 的元數(shù)據(jù)包括下面幾部分。

prev_len,表示前一個 entry 的長度。prev_len 有兩種取值情況:1 字節(jié)或 5 字節(jié)。取值 1 字節(jié)時,表示上一個 entry 的長度小于 254 字節(jié)。雖然 1 字節(jié)的值能表示的數(shù)值范圍是 0 到 255,但是壓縮列表中 zlend 的取值默認是 255,因此,就默認用 255 表示整個壓縮列表的結(jié)束,其他表示長度的地方就不能再用 255 這個值了。所以,當(dāng)上一個 entry 長度小于 254 字節(jié)時,prev_len 取值為 1 字節(jié),否則,就取值為 5 字節(jié)。

len:表示自身長度,4 字節(jié);

encoding:表示編碼方式,1 字節(jié);

content:保存實際數(shù)據(jù)。

這些 entry 會挨個兒放置在內(nèi)存中,不需要再用額外的指針進行連接,這樣就可以節(jié)省指針?biāo)加玫目臻g。

我們以保存圖片存儲對象 ID 為例,來分析一下壓縮列表是如何節(jié)省內(nèi)存空間的。

每個 entry 保存一個圖片存儲對象 ID(8 字節(jié)),此時,每個 entry 的 prev_len 只需要 1 個字節(jié)就行,因為每個 entry 的前一個 entry 長度都只有 8 字節(jié),小于 254 字節(jié)。這樣一來,一個圖片的存儲對象 ID 所占用的內(nèi)存大小是 14 字節(jié)(1+4+1+8=14),實際分配 16 字節(jié)。

Redis 基于壓縮列表實現(xiàn)了 List、Hash 和 Sorted Set 這樣的集合類型,這樣做的最大好處就是節(jié)省了 dictEntry 的開銷。當(dāng)你用 String 類型時,一個鍵值對就有一個 dictEntry,要用 32 字節(jié)空間。但采用集合類型時,一個 key 就對應(yīng)一個集合的數(shù)據(jù),能保存的數(shù)據(jù)多了很多,但也只用了一個 dictEntry,這樣就節(jié)省了內(nèi)存。

這個方案聽起來很好,但還存在一個問題:在用集合類型保存鍵值對時,一個鍵對應(yīng)了一個集合的數(shù)據(jù),但是在我們的場景中,一個圖片 ID 只對應(yīng)一個圖片的存儲對象 ID,我們該怎么用集合類型呢?換句話說,在一個鍵對應(yīng)一個值(也就是單值鍵值對)的情況下,我們該怎么用集合類型來保存這種單值鍵值對呢?

如何用集合類型保存單值的鍵值對?

在保存單值的鍵值對時,可以采用基于 Hash 類型的二級編碼方法。這里說的二級編碼,就是把一個單值的數(shù)據(jù)拆分成兩部分,前一部分作為 Hash 集合的 key,后一部分作為 Hash 集合的 value,這樣一來,我們就可以把單值數(shù)據(jù)保存到 Hash 集合中了。

以圖片 ID 1101000060 和圖片存儲對象 ID 3302000080 為例,我們可以把圖片 ID 的前 7 位(1101000)作為 Hash 類型的鍵,把圖片 ID 的最后 3 位(060)和圖片存儲對象 ID 分別作為 Hash 類型值中的 key 和 value。

按照這種設(shè)計方法,我在 Redis 中插入了一組圖片 ID 及其存儲對象 ID 的記錄,并且用 info 命令查看了內(nèi)存開銷,我發(fā)現(xiàn),增加一條記錄后,內(nèi)存占用只增加了 16 字節(jié),如下所示:

127.0.0.1:6379> info memory
# Memory
used_memory:1039120
127.0.0.1:6379> hset 1101000 060 3302000080
(integer) 1
127.0.0.1:6379> info memory
# Memory
used_memory:1039136

在使用 String 類型時,每個記錄需要消耗 64 字節(jié),這種方式卻只用了 16 字節(jié),所使用的內(nèi)存空間是原來的 1/4,滿足了我們節(jié)省內(nèi)存空間的需求。

不過,你可能也會有疑惑:“二級編碼一定要把圖片 ID 的前 7 位作為 Hash 類型的鍵,把最后 3 位作為 Hash 類型值中的 key 嗎?”其實,二級編碼方法中采用的 ID 長度是有講究的。

Redis Hash 類型的兩種底層實現(xiàn)結(jié)構(gòu),分別是壓縮列表和哈希表。

那么,Hash 類型底層結(jié)構(gòu)什么時候使用壓縮列表,什么時候使用哈希表呢?其實,Hash 類型設(shè)置了用壓縮列表保存數(shù)據(jù)時的兩個閾值,一旦超過了閾值,Hash 類型就會用哈希表來保存數(shù)據(jù)了。

這兩個閾值分別對應(yīng)以下兩個配置項:

hash-max-ziplist-entries:表示用壓縮列表保存時哈希集合中的最大元素個數(shù)。

hash-max-ziplist-value:表示用壓縮列表保存時哈希集合中單個元素的最大長度。

如果我們往 Hash 集合中寫入的元素個數(shù)超過了 hash-max-ziplist-entries,或者寫入的單個元素大小超過了 hash-max-ziplist-value,Redis 就會自動把 Hash 類型的實現(xiàn)結(jié)構(gòu)由壓縮列表轉(zhuǎn)為哈希表。

一旦從壓縮列表轉(zhuǎn)為了哈希表,Hash 類型就會一直用哈希表進行保存,而不會再轉(zhuǎn)回壓縮列表了。在節(jié)省內(nèi)存空間方面,哈希表就沒有壓縮列表那么高效了。

為了能充分使用壓縮列表的精簡內(nèi)存布局,我們一般要控制保存在 Hash 集合中的元素個數(shù)。所以,在剛才的二級編碼中,我們只用圖片 ID 最后 3 位作為 Hash 集合的 key,也就保證了 Hash 集合的元素個數(shù)不超過 1000,同時,我們把 hash-max-ziplist-entries 設(shè)置為 1000,這樣一來,Hash 集合就可以一直使用壓縮列表來節(jié)省內(nèi)存空間了。

小結(jié)

在這篇文章中,我們將顛覆以往對 String 數(shù)據(jù)類型的傳統(tǒng)認知。以前,String 被視為一種“萬金油”,在各種場合都被廣泛使用。然而,當(dāng)存儲的鍵值對數(shù)據(jù)本身占用的內(nèi)存空間較小時,String 類型的元數(shù)據(jù)開銷占據(jù)了主導(dǎo)地位。這些開銷包括 RedisObject 結(jié)構(gòu)、SDS 結(jié)構(gòu)以及dictEntry 結(jié)構(gòu)的內(nèi)存消耗。

為了應(yīng)對這種情況,我們可以采用壓縮列表(ziplist)來存儲數(shù)據(jù)。當(dāng)然,當(dāng)使用 Hash 這種集合類型來保存單一鍵值對數(shù)據(jù)時,我們需要將單一值數(shù)據(jù)分割成兩部分,分別作為 Hash 集合的鍵和值。就像之前案例中使用了二級編碼來表示圖片 ID那樣,我們鼓勵你將這一方法應(yīng)用到你的具體場景中。這不僅可以減少內(nèi)存開銷,還能提高 Redis 的性能。希望這個解決方案對你的應(yīng)用有所幫助。

責(zé)任編輯:武曉燕 來源: 碼農(nóng)本農(nóng)
相關(guān)推薦

2020-04-28 17:13:12

箭頭函數(shù)ES6函數(shù)

2020-11-02 17:34:22

數(shù)據(jù)分析人工智能技術(shù)

2023-06-30 07:19:25

電源供電顯卡

2014-10-20 10:53:13

ArubaWi-Fi無線網(wǎng)絡(luò)

2021-06-29 07:19:06

Redis容器化K8S

2021-06-29 15:39:16

容器技術(shù)Redis

2023-03-13 00:21:21

調(diào)試器斷點開發(fā)者

2024-12-26 09:58:18

2023-10-25 16:36:06

數(shù)字化轉(zhuǎn)型IT系統(tǒng)

2025-04-24 08:25:00

2021-08-16 13:44:37

手機電子日本

2021-12-15 10:20:08

緩存架構(gòu)開發(fā)

2021-01-28 14:41:08

麥肯錫數(shù)字化項目

2021-11-08 11:21:18

redis 淘汰算法

2019-10-15 09:46:46

機器學(xué)習(xí)人工智能計算機

2018-07-01 08:34:09

緩存數(shù)據(jù)服務(wù)

2025-01-20 08:10:00

AI模型研究

2022-06-07 17:01:31

UI框架前端

2021-05-13 09:27:13

JavaThreadLocal線程

2013-02-22 09:43:41

面向?qū)ο?/a>面向?qū)ο缶幊?/a>
點贊
收藏

51CTO技術(shù)棧公眾號