日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

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

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

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

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

photo_id: 1101000051

photo_obj_id: 3301000051

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Redis中萬(wàn)金油的String,為什么不好用了?圖片

buf:字節(jié)數(shù)組,保存實(shí)際數(shù)據(jù)。為了表示字節(jié)數(shù)組的結(jié)束,Redis 會(huì)自動(dòng)在數(shù)組最后加一個(gè)“”,這就會(huì)額外占用 1 個(gè)字節(jié)的開(kāi)銷。

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

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

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

另外,對(duì)于 String 類型來(lái)說(shuō),除了 SDS 的額外開(kāi)銷,還有一個(gè)來(lái)自于 RedisObject 結(jié)構(gòu)體的開(kāi)銷。

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

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

Redis中萬(wàn)金油的String,為什么不好用了?圖片

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

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

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

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

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

Redis中萬(wàn)金油的String,為什么不好用了?圖片

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

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

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

Redis中萬(wàn)金油的String,為什么不好用了?圖片

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

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

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

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

你看,明明有效信息只有 16 字節(jié),使用 String 類型保存時(shí),卻需要 64 字節(jié)的內(nèi)存空間,有 48 字節(jié)都沒(méi)有用于保存實(shí)際的數(shù)據(jù)。我們來(lái)?yè)Q算下,如果要保存的圖片有 1 億張,那么 1 億條的圖片 ID 記錄就需要 6.4GB 內(nèi)存空間,其中有 4.8GB 的內(nèi)存空間都用來(lái)保存元數(shù)據(jù)了,額外的內(nèi)存空間開(kāi)銷很大。那么,有沒(mé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)成。表頭有三個(gè)字段 zlbytes、zltAIl 和 zllen,分別表示列表長(zhǎng)度、列表尾的偏移量,以及列表中的 entry 個(gè)數(shù)。壓縮列表尾還有一個(gè) zlend,表示列表結(jié)束。

Redis中萬(wàn)金油的String,為什么不好用了?圖片

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

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

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

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

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

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

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

每個(gè) entry 保存一個(gè)圖片存儲(chǔ)對(duì)象 ID(8 字節(jié)),此時(shí),每個(gè) entry 的 prev_len 只需要 1 個(gè)字節(jié)就行,因?yàn)槊總€(gè) entry 的前一個(gè) entry 長(zhǎng)度都只有 8 字節(jié),小于 254 字節(jié)。這樣一來(lái),一個(gè)圖片的存儲(chǔ)對(duì)象 ID 所占用的內(nèi)存大小是 14 字節(jié)(1+4+1+8=14),實(shí)際分配 16 字節(jié)。

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

這個(gè)方案聽(tīng)起來(lái)很好,但還存在一個(gè)問(wèn)題:在用集合類型保存鍵值對(duì)時(shí),一個(gè)鍵對(duì)應(yīng)了一個(gè)集合的數(shù)據(jù),但是在我們的場(chǎng)景中,一個(gè)圖片 ID 只對(duì)應(yīng)一個(gè)圖片的存儲(chǔ)對(duì)象 ID,我們?cè)撛趺从眉项愋湍兀繐Q句話說(shuō),在一個(gè)鍵對(duì)應(yīng)一個(gè)值(也就是單值鍵值對(duì))的情況下,我們?cè)撛趺从眉项愋蛠?lái)保存這種單值鍵值對(duì)呢?

如何用集合類型保存單值的鍵值對(duì)?

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

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

按照這種設(shè)計(jì)方法,我在 Redis 中插入了一組圖片 ID 及其存儲(chǔ)對(duì)象 ID 的記錄,并且用 info 命令查看了內(nèi)存開(kā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 類型時(shí),每個(gè)記錄需要消耗 64 字節(jié),這種方式卻只用了 16 字節(jié),所使用的內(nèi)存空間是原來(lái)的 1/4,滿足了我們節(jié)省內(nèi)存空間的需求。

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

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

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

這兩個(gè)閾值分別對(duì)應(yīng)以下兩個(gè)配置項(xiàng):

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

hash-max-ziplist-value:表示用壓縮列表保存時(shí)哈希集合中單個(gè)元素的最大長(zhǎng)度。

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

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

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

小結(jié)

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

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

分享到:
標(biāo)簽:Redis
用戶無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定