<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          “萬金油”的String,為什么不好用了?

          共 6279字,需瀏覽 13分鐘

           ·

          2021-10-29 12:37

          從今天開始,我們就要進入“實踐篇”了。接下來,我們會用5節(jié)課的時間學習“數(shù)據(jù)結構”。我會介紹節(jié)省內(nèi)存開銷以及保存和統(tǒng)計海量數(shù)據(jù)的數(shù)據(jù)類型及其底層數(shù)據(jù)結構,還會圍繞典型的應用場景(例如地址位置查詢、時間序列數(shù)據(jù)庫讀寫和消息隊列存取),跟你分享使用Redis的數(shù)據(jù)類型和module擴展功能來滿足需求的具體方案。

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

          先跟你分享一個我曾經(jīng)遇到的需求。

          當時,我們要開發(fā)一個圖片存儲系統(tǒng),要求這個系統(tǒng)能快速地記錄圖片ID和圖片在存儲系統(tǒng)中保存時的ID(可以直接叫作圖片存儲對象ID)。同時,還要能夠根據(jù)圖片ID快速查找到圖片存儲對象ID。

          因為圖片數(shù)量巨大,所以我們就用10位數(shù)來表示圖片ID和圖片存儲對象ID,例如,圖片ID為1101000051,它在存儲系統(tǒng)中對應的ID號是3301000051。

          photo_id: 1101000051
          photo_obj_id: 3301000051
          復制代碼

          可以看到,圖片ID和圖片存儲對象ID正好一一對應,是典型的“鍵-單值”模式。所謂的“單值”,就是指鍵值對中的值就是一個值,而不是一個集合,這和String類型提供的“一個鍵對應一個值的數(shù)據(jù)”的保存形式剛好契合。

          而且,String類型可以保存二進制字節(jié)流,就像“萬金油”一樣,只要把數(shù)據(jù)轉成二進制字節(jié)數(shù)組,就可以保存了。

          所以,我們的第一個方案就是用String保存數(shù)據(jù)。我們把圖片ID和圖片存儲對象ID分別作為鍵值對的key和value來保存,其中,圖片存儲對象ID用了String類型。

          剛開始,我們保存了1億張圖片,大約用了6.4GB的內(nèi)存。但是,隨著圖片數(shù)據(jù)量的不斷增加,我們的Redis內(nèi)存使用量也在增加,結果就遇到了大內(nèi)存Redis實例因為生成RDB而響應變慢的問題。很顯然,String類型并不是一種好的選擇,我們還需要進一步尋找能節(jié)省內(nèi)存開銷的數(shù)據(jù)類型方案。

          在這個過程中,我深入地研究了String類型的底層結構,找到了它內(nèi)存開銷大的原因,對“萬金油”的String類型有了全新的認知:String類型并不是適用于所有場合的,它有一個明顯的短板,就是它保存數(shù)據(jù)時所消耗的內(nèi)存空間較多。

          同時,我還仔細研究了集合類型的數(shù)據(jù)結構。我發(fā)現(xiàn),集合類型有非常節(jié)省內(nèi)存空間的底層實現(xiàn)結構,但是,集合類型保存的數(shù)據(jù)模式,是一個鍵對應一系列值,并不適合直接保存單值的鍵值對。所以,我們就使用二級編碼的方法,實現(xiàn)了用集合類型保存單值鍵值對,Redis實例的內(nèi)存空間消耗明顯下降了。

          這節(jié)課,我就把在解決這個問題時學到的經(jīng)驗和方法分享給你,包括String類型的內(nèi)存空間消耗在哪兒了、用什么數(shù)據(jù)結構可以節(jié)省內(nèi)存,以及如何用集合類型保存單值鍵值對。如果你在使用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ù)。當實際保存的數(shù)據(jù)較小時,元數(shù)據(jù)的空間開銷就顯得比較大了,有點“喧賓奪主”的意思。

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

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

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

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

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

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

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

          另外,對于String類型來說,除了SDS的額外開銷,還有一個來自于RedisObject結構體的開銷。

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

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

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

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

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

          當然,當字符串大于44字節(jié)時,SDS的數(shù)據(jù)量就開始變多了,Redis就不再把SDS和RedisObject布局在一起了,而是會給SDS分配獨立的空間,并用指針指向SDS結構。這種布局方式被稱為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é)去哪兒了呢?

          我在第2講中說過,Redis會使用一個全局哈希表保存所有鍵值對,哈希表的每一項是一個dictEntry的結構體,用來指向一個鍵值對。dictEntry結構中有三個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結構就占用了32字節(jié)。

          好了,到這兒,你應該就能理解,為什么用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é)省內(nèi)存?

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

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

          壓縮列表之所以能節(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表示整個壓縮列表的結束,其他表示長度的地方就不能再用255這個值了。所以,當上一個entry長度小于254字節(jié)時,prev_len取值為1字節(jié),否則,就取值為5字節(jié)。

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

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

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

          這些entry會挨個兒放置在內(nèi)存中,不需要再用額外的指針進行連接,這樣就可以節(jié)省指針所占用的空間。

          我們以保存圖片存儲對象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的開銷。當你用String類型時,一個鍵值對就有一個dictEntry,要用32字節(jié)空間。但采用集合類型時,一個key就對應一個集合的數(shù)據(jù),能保存的數(shù)據(jù)多了很多,但也只用了一個dictEntry,這樣就節(jié)省了內(nèi)存。

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

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

          在保存單值的鍵值對時,可以采用基于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。

          按照這種設計方法,我在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長度是有講究的。

          在第2講中,我介紹過Redis Hash類型的兩種底層實現(xiàn)結構,分別是壓縮列表和哈希表。

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

          這兩個閾值分別對應以下兩個配置項:

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

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

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

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

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

          小結

          這節(jié)課,我們打破了對String的認知誤區(qū),以前,我們認為String是“萬金油”,什么場合都適用,但是,在保存的鍵值對本身占用的內(nèi)存空間不大時(例如這節(jié)課里提到的的圖片ID和圖片存儲對象ID),String類型的元數(shù)據(jù)開銷就占據(jù)主導了,這里面包括了RedisObject結構、SDS結構、dictEntry結構的內(nèi)存開銷。

          針對這種情況,我們可以使用壓縮列表保存數(shù)據(jù)。當然,使用Hash這種集合類型保存單值鍵值對的數(shù)據(jù)時,我們需要將單值數(shù)據(jù)拆分成兩部分,分別作為Hash集合的鍵和值,就像剛才案例中用二級編碼來表示圖片ID,希望你能把這個方法用到自己的場景中。

          最后,我還想再給你提供一個小方法:如果你想知道鍵值對采用不同類型保存時的內(nèi)存開銷,可以在這個網(wǎng)址里輸入你的鍵值對長度和使用的數(shù)據(jù)類型,這樣就能知道實際消耗的內(nèi)存大小了。建議你把這個小工具用起來,它可以幫助你充分地節(jié)省內(nèi)存。

          每課一問

          按照慣例,給你提個小問題:除了String類型和Hash類型,你覺得,還有其他合適的類型可以應用在這節(jié)課所說的保存圖片的例子嗎?


          作者:一只睡著的貓
          鏈接:https://juejin.cn/post/7022927348240482340
          來源:稀土掘金
          著作權歸作者所有。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。



          瀏覽 41
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  无码性爱视频在线观看 | 中国A毛片 | 国产精品自在拍在线拍 | ww亚洲ww | 国产黄色电影在线看 |