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) 的抽象類型。其好處有:

整體結構如下圖所示

SDS 相比較原先的 C 語言字符串,更加易用,但是帶來的代價就是佔用了更多的內存空間。除了 SDS 結構的內存空間損耗外,Redis 對每個保存的數據都使用了一個叫做 RedisObject 的結構體來統一記錄對應數據的元數據。元數據中記錄了最後一次訪問的時間、被引用的次數等。每個 RedisObject 指向實際的數據,結構如下圖所示:

可以看到一個 RedisObject 包含了一個 8 字節的元數據和 8 個字節的指針。但爲了進一步節省內存空間,Redis 還對整數類型的數據和 SDS 的內存佈局做了專門的設計,對不同大小的數據使用了不同的編碼模式。

字符串的三種編碼方式

爲了節省空間 Redis 還專門對不同類型的字符串數據做了不同的處理,並稱之爲三種不同的編碼模式,即 int 編碼、embstr 編碼、raw 編碼。如圖所示:

可以看到當我們保存一個整數的 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 的數據包含:

每個 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 集合的元素的個數不會超過 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