Redis String 結構解析及內存使用優化
前言
Redis 是一個高性能的 key-value 數據庫,由於其易用、性能高、擴展性好等特點,已經成爲後端內存數據庫的業界標準。使用 Redis 進行日常開發時,最常使用的數據結構應當是 String,但 String 也不是 "萬金油",使用不當也會造成很多內存上的浪費。本文會解析 String 數據是如何保存的,並分析其佔用內存的原因,以及說明如何減少內存的使用。
爲什麼 String 類型內存開銷大
String 類型的內存空間消耗
正常情況下,一個 int 類型的數據需要佔用 4 個字節的內存空間,而保存一個 key-value 鍵值對數據則需要 8 個字節的內存空間,但在 redis 中保存鍵爲: 110156,值爲: 330155 的鍵值對時卻用了 48 個字節的內存空間。如下圖所示:
實際上,當 redis 保存一個 String 類型的數據的時候,除了數據本身外還需要額外的內存空間記錄數據長度、空間等使用信息,這些信息也叫做元數據。當需要存儲這類純數字的 key-value 鍵值對的時候,元數據信息佔用的空間就會比本身需要存儲的數據更多,所以 String 類型並不適合存儲空間較小的數據。那麼 String 的底層結構又是怎麼存儲的呢?
簡單動態字符串
Redis 中沒有直接使用 C 語言的字符串,而是自己構建了一種名爲簡單動態字符串 (Simple Dynamic String,SDS) 的抽象類型。其好處有:
-
常數複雜度獲取字符串長度:C 語言中的字符串並未記錄字符串長度需要遍歷去獲取。SDS 記錄本身字符串的一個長度,所以可以直接獲取。
-
杜絕緩衝區溢出:C 語言字符串本身不記錄長度,導致有可能修改字符串內容時,內存溢出到下一個字符串的內存空間。而 SDS 會進行自動擴容。
-
減少修改字符串時帶來的內存重分配的次數: C 語言字符串在修改字符串長度時需要頻繁的變換。
整體結構如下圖所示
-
buf :字節數組,保存實際數據。爲了表示字節數組的結束,Redis 會自動在數組最後加一個 "/0"。
-
len:表示 buf 的已用長度,佔用 4 個字節。
-
alloc:表示 buf 的實際分配長度,佔用 4 個字節。
SDS 相比較原先的 C 語言字符串,更加易用,但是帶來的代價就是佔用了更多的內存空間。除了 SDS 結構的內存空間損耗外,Redis 對每個保存的數據都使用了一個叫做 RedisObject 的結構體來統一記錄對應數據的元數據。元數據中記錄了最後一次訪問的時間、被引用的次數等。每個 RedisObject 指向實際的數據,結構如下圖所示:
可以看到一個 RedisObject 包含了一個 8 字節的元數據和 8 個字節的指針。但爲了進一步節省內存空間,Redis 還對整數類型的數據和 SDS 的內存佈局做了專門的設計,對不同大小的數據使用了不同的編碼模式。
字符串的三種編碼方式
爲了節省空間 Redis 還專門對不同類型的字符串數據做了不同的處理,並稱之爲三種不同的編碼模式,即 int 編碼、embstr 編碼、raw 編碼。如圖所示:
-
int 編碼:當保存的是整數時,RedisObject 中的指針就直接賦值爲整數了,節省了指針的開銷。
-
embstr 編碼:當要保存的字符串小於 44 字節時,RedisObject 和 SDS 是一塊連續的區域,避免內存碎片。
-
raw 編碼:當要保存的字符串大於 44 字節時,SDS 的數據量變多,會給 SDS 獨立的分配內存空間,然後用指針指向 SDS。
可以看到當我們保存一個整數的 key-value 鍵值對的時候,實際使用了 12 字節。但是我們看到的例子中使用了 48 個字節,剩下的 36 個字節被什麼消耗了呢?
全局哈希表
實際上,Redis 會使用一個全局的哈希表保存所有的鍵值對,使用全局哈希表最大的好處就是可以用 O(1) 的時間複雜度來快速查找到鍵值對。全局的哈希表構造如下:
Redis 會使用一個全局的哈希表保存所有的鍵值對,哈希表的每一項是一個 Entry ,一個 Enrty 中有三個指針,分別用於指向 key、value 和下一個 Entry,每個對象分別佔用 8 個指針,總共佔用 24 個字節。如圖所示:
而用了 32 個字節的原因在於 Redis 使用的內存分配庫 jemalloc,jemalloc 在分配內存時,會根據申請的字節數 N,找一個比 N 大,但是最接近 N² 的冪次數作爲空間。所以申請了 24 字節,則最終會分配 32 個字節。
由此可見,有效的信息只佔用 12 個字節,但是卻需要 48 個字節。那麼如果有一億個這樣的數據的話,就需要 4.8 個 GB 的空間,額外空間的損耗是 3.2 個 GB。內存佔用過大,需要有一種更加節省內存空間的存儲方式
更加節省內存的數據結構
在 redis 中有一種數據結構叫做壓縮列表 (ziplist), 這種數據結構可以更加的節省內存。壓縮列表的表頭部分由 zlbytes、zltail 和 zllen 組成,分別標識列表長度、列表尾的偏移量。表的中間部分是 entry ,表示保存實際的數據。表結尾是 zlend,表示列表的結束。結構如下圖所示:
entry 是連續的,不用額外的指針進行連接,這樣節省了指針所佔用的空間。每個 entry 的數據包含:
-
len:表示自身長度,4 字節。
-
content:保存實際數據。
每個 entry 保存一個圖片存儲對象 ID 在 int 範圍內( 4 字節), 記錄自身長度 len 需要 4 個字節,所以 key 加上 value 佔用的字節應該是 (4+4)*2 = 16 字節。可以看到相比於使用 String 一次佔用 48 個字節,使用壓縮列表的方式只用了 16 個字節,節省了 32 個字節,這大數據量的情況下能節省不少內存空間。
Redis 基於壓縮列表實現了 Hash、List、 Sortd Set 等集合類型。那麼如何利用壓縮列表來保存非集合類型的數據?
用集合類型保存單值的鍵值對
如果是單值的鍵值對,可以採用基於 Hash 類型的二級編碼方法。比如把一個單值的數據拆成兩部分,前一部分作爲 Hash 集合的 key, 後一部分作爲 Hash 集合的 value。
例如對於要存儲 key:110156,value:330155 的數據,可以將 110 作爲 Hash 的鍵將 "156" 和 "330155" 分別作爲 Hash 類型值中的 key 和 value ,實際只使用了 16 字節,如下圖所示:
但實際上 Hash 底層數據在超過某個閾值的時候,就由壓縮列表轉換爲了哈希表,所以說其中存儲的數字長度也十分有講究。控制這個閾值的由下面兩個 Redis 配置參數來控制:
-
hash-max-ziplist-entries:表示用壓縮列表保存時哈希集合中的最大元素個數。
-
hash-max-ziplist-value:表示用壓縮列表保存時哈希集合中單個元素的最大長度。
使用壓縮列表的時候,如果像例子中的保存的數值是三位數的數字,那麼也就保證了 Hash 集合的元素的個數不會超過 1000,所以我們設置 hash-max-ziplist-entries 值爲 1000,這樣一來就可以使用壓縮列表來節省內存空間了。
小結
本次主要介紹了 redis 在內存中使用的是簡單動態字符串 (SDS),以及它的三種編碼方式,還有它耗費內存的原因在於 RedisObject 和本身 SDS 的結構。我們可以使用壓縮列表來節省內存,剖析了其節省內存的原因。並使用 Hash 的數據結構給出了實際的節省內存的案例,大家可以根據實際的業務,合理的設計緩存的存儲結構,達到節省內存的目的。這裏可以推薦一個網址可以用來大致計算 Redis 的內存損耗 redis 內存損耗計算
參考
memory-optimization
Redis 核心技術與實戰
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/3UIvhxRGfSjrz7jE-VDc2A