深入瞭解一下Redis的記憶體模型!

lvxfcjf發表於2021-09-09

一前言

Redis是目前最火爆的記憶體資料庫之一,透過在記憶體中讀寫資料,大大提高了讀寫速度,可以說Redis是實現網站高併發不可或缺的一部分。

我們使用Redis時,會接觸Redis的5種物件型別(字串、雜湊、列表、集合、有序集合),豐富的型別是Redis相對於Memcached等的一大優勢。在瞭解Redis的5種物件型別的用法和特點的基礎上,進一步瞭解Redis的記憶體模型,對Redis的使用有很大幫助,例如:

1、估算Redis記憶體使用量。目前為止,記憶體的使用成本仍然相對較高,使用記憶體不能無所顧忌;根據需求合理的評估Redis的記憶體使用量,選擇合適的機器配置,可以在滿足需求的情況下節約成本。

2、最佳化記憶體佔用。瞭解Redis記憶體模型可以選擇更合適的資料型別和編碼,更好的利用Redis記憶體。

3、分析解決問題。當Redis出現阻塞、記憶體佔用等問題時,儘快發現導致問題的原因,便於分析解決問題。

這篇文章主要介紹Redis的記憶體模型(以3.0為例),包括Redis佔用記憶體的情況及如何查詢、不同的物件型別在記憶體中的編碼方式、記憶體分配器(jemalloc)、簡單動態字串(SDS)、RedisObject等;然後在此基礎上介紹幾個Redis記憶體模型的應用。

在後面的文章中,會陸續介紹關於Redis高可用的內容,包括主從複製、哨兵、叢集等等,歡迎關注。

目錄

工欲善其事必先利其器,在說明Redis記憶體之前首先說明如何統計Redis使用記憶體的情況。

在客戶端透過redis-cli連線伺服器後(後面如無特殊說明,客戶端一律使用redis-cli),透過info命令可以檢視記憶體使用情況:

1info memory

圖片描述

其中,info命令可以顯示redis伺服器的許多資訊,包括伺服器基本資訊、CPU、記憶體、持久化、客戶端連線資訊等等;memory是引數,表示只顯示記憶體相關的資訊。

返回結果中比較重要的幾個說明如下:

(1)used_memory:Redis分配器分配的記憶體總量(單位是位元組),包括使用的虛擬記憶體(即swap);Redis分配器後面會介紹。used_memory_human只是顯示更友好。

(2)used_memory_rss:Redis程式佔據作業系統的記憶體(單位是位元組),與top及ps命令看到的值是一致的;除了分配器分配的記憶體之外,used_memory_rss還包括程式執行本身需要的記憶體、記憶體碎片等,但是不包括虛擬記憶體。

因此,used_memory和used_memory_rss,前者是從Redis角度得到的量,後者是從作業系統角度得到的量。二者之所以有所不同,一方面是因為記憶體碎片和Redis程式執行需要佔用記憶體,使得前者可能比後者小,另一方面虛擬記憶體的存在,使得前者可能比後者大。

由於在實際應用中,Redis的資料量會比較大,此時程式執行佔用的記憶體與Redis資料量和記憶體碎片相比,都會小得多;因此used_memory_rss和used_memory的比例,便成了衡量Redis記憶體碎片率的引數;這個引數就是mem_fragmentation_ratio。

(3)mem_fragmentation_ratio:記憶體碎片比率,該值是used_memory_rss / used_memory的比值。

mem_fragmentation_ratio一般大於1,且該值越大,記憶體碎片比例越大。mem_fragmentation_ratio<1,說明Redis使用了虛擬記憶體,由於虛擬記憶體的媒介是磁碟,比記憶體速度要慢很多,當這種情況出現時,應該及時排查,如果記憶體不足應該及時處理,如增加Redis節點、增加Redis伺服器的記憶體、最佳化應用等。

一般來說,mem_fragmentation_ratio在1.03左右是比較健康的狀態(對於jemalloc來說);上面截圖中的mem_fragmentation_ratio值很大,是因為還沒有向Redis中存入資料,Redis程式本身執行的記憶體使得used_memory_rss 比used_memory大得多。

(4)mem_allocator:Redis使用的記憶體分配器,在編譯時指定;可以是 libc 、jemalloc或者tcmalloc,預設是jemalloc;截圖中使用的便是預設的jemalloc。

Redis作為記憶體資料庫,在記憶體中儲存的內容主要是資料(鍵值對);透過前面的敘述可以知道,除了資料以外,Redis的其他部分也會佔用記憶體。

Redis的記憶體佔用主要可以劃分為以下幾個部分:

作為資料庫,資料是最主要的部分;這部分佔用的記憶體會統計在used_memory中。

Redis使用鍵值對儲存資料,其中的值(物件)包括5種型別,即字串、雜湊、列表、集合、有序集合。這5種型別是Redis對外提供的,實際上,在Redis內部,每種型別可能有2種或更多的內部編碼實現;此外,Redis在儲存物件時,並不是直接將資料扔進記憶體,而是會對物件進行各種包裝:如redisObject、SDS等;這篇文章後面將重點介紹Redis中資料儲存的細節。

Redis主程式本身執行肯定需要佔用記憶體,如程式碼、常量池等等;這部分記憶體大約幾兆,在大多數生產環境中與Redis資料佔用的記憶體相比可以忽略。這部分記憶體不是由jemalloc分配,因此不會統計在used_memory中。

補充說明:除了主程式外,Redis建立的子程式執行也會佔用記憶體,如Redis執行AOF、RDB重寫時建立的子程式。當然,這部分記憶體不屬於Redis程式,也不會統計在used_memory和used_memory_rss中。

緩衝記憶體包括客戶端緩衝區、複製積壓緩衝區、AOF緩衝區等;其中,客戶端緩衝儲存客戶端連線的輸入輸出緩衝;複製積壓緩衝用於部分複製功能;AOF緩衝區用於在進行AOF重寫時,儲存最近的寫入命令。在瞭解相應功能之前,不需要知道這些緩衝的細節;這部分記憶體由jemalloc分配,因此會統計在used_memory中。

記憶體碎片是Redis在分配、回收實體記憶體過程中產生的。例如,如果對資料的更改頻繁,而且資料之間的大小相差很大,可能導致redis釋放的空間在實體記憶體中並沒有釋放,但redis又無法有效利用,這就形成了記憶體碎片。記憶體碎片不會統計在used_memory中。

記憶體碎片的產生與對資料進行的操作、資料的特點等都有關;此外,與使用的記憶體分配器也有關係:如果記憶體分配器設計合理,可以儘可能的減少記憶體碎片的產生。後面將要說到的jemalloc便在控制記憶體碎片方面做的很好。

如果Redis伺服器中的記憶體碎片已經很大,可以透過安全重啟的方式減小記憶體碎片:因為重啟之後,Redis重新從備份檔案中讀取資料,在記憶體中進行重排,為每個資料重新選擇合適的記憶體單元,減小記憶體碎片。

關於Redis資料儲存的細節,涉及到記憶體分配器(如jemalloc)、簡單動態字串(SDS)、5種物件型別及內部編碼、redisObject。在講述具體內容之前,先說明一下這幾個概念之間的關係。

下圖是執行set hello world時,所涉及到的資料模型。

圖片描述

圖片來源:

(1)dictEntry:Redis是Key-Value資料庫,因此對每個鍵值對都會有一個dictEntry,裡面儲存了指向Key和Value的指標;next指向下一個dictEntry,與本Key-Value無關。

(2)Key:圖中右上角可見,Key(”hello”)並不是直接以字串儲存,而是儲存在SDS結構中。

(3)redisObject:Value(“world”)既不是直接以字串儲存,也不是像Key一樣直接儲存在SDS中,而是儲存在redisObject中。實際上,不論Value是5種型別的哪一種,都是透過redisObject來儲存的;而redisObject中的type欄位指明瞭Value物件的型別,ptr欄位則指向物件所在的地址。不過可以看出,字串物件雖然經過了redisObject的包裝,但仍然需要透過SDS儲存。

實際上,redisObject除了type和ptr欄位以外,還有其他欄位圖中沒有給出,如用於指定物件內部編碼的欄位;後面會詳細介紹。

(4)jemalloc:無論是DictEntry物件,還是redisObject、SDS物件,都需要記憶體分配器(如jemalloc)分配記憶體進行儲存。以DictEntry物件為例,有3個指標組成,在64位機器下佔24個位元組,jemalloc會為它分配32位元組大小的記憶體單元。

下面來分別介紹jemalloc、redisObject、SDS、物件型別及內部編碼。

Redis在編譯時便會指定記憶體分配器;記憶體分配器可以是 libc 、jemalloc或者tcmalloc,預設是jemalloc。

jemalloc作為Redis的預設記憶體分配器,在減小記憶體碎片方面做的相對比較好。jemalloc在64位系統中,將記憶體空間劃分為小、大、巨大三個範圍;每個範圍內又劃分了許多小的記憶體塊單位;當Redis儲存資料時,會選擇大小最合適的記憶體塊進行儲存。

jemalloc劃分的記憶體單元如下圖所示:

圖片描述

圖片來源:http://blog.csdn.net/zhengpeitao/article/details/76573053

例如,如果需要儲存大小為130位元組的物件,jemalloc會將其放入160位元組的記憶體單元中。

前面說到,Redis物件有5種型別;無論是哪種型別,Redis都不會直接儲存,而是透過redisObject物件進行儲存。

redisObject物件非常重要,Redis物件的型別、內部編碼、記憶體回收、共享物件等功能,都需要redisObject支援,下面將透過redisObject的結構來說明它是如何起作用的。

redisObject的定義如下(不同版本的Redis可能稍稍有所不同):

1

2

3

4

5

6

7

typedef struct redisObject {

  unsigned type:4;

  unsigned encoding:4;

unsigned lru:REDIS_LRU_BITS;

  int refcount;

  void *ptr;

} robj;

redisObject的每個欄位的含義和作用如下:

(1)type

type欄位表示物件的型別,佔4個位元;目前包括REDIS_STRING(字串)、REDIS_LIST (列表)、REDIS_HASH(雜湊)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。

當我們執行type命令時,便是透過讀取RedisObject的type欄位獲得物件的型別;如下圖所示:

圖片描述

(2)encoding

encoding表示物件的內部編碼,佔4個位元。

對於Redis支援的每種型別,都有至少兩種內部編碼,例如對於字串,有int、embstr、raw三種編碼。透過encoding屬性,Redis可以根據不同的使用場景來為物件設定不同的編碼,大大提高了Redis的靈活性和效率。以列表物件為例,有壓縮列表和雙端連結串列兩種編碼方式;如果列表中的元素較少,Redis傾向於使用壓縮列表進行儲存,因為壓縮列表佔用記憶體更少,而且比雙端連結串列可以更快載入;當列表物件元素較多時,壓縮列表就會轉化為更適合儲存大量元素的雙端連結串列。

透過object encoding命令,可以檢視物件採用的編碼方式,如下圖所示:

圖片描述

5種物件型別對應的編碼方式以及使用條件,將在後面介紹。

(3)lru

lru記錄的是物件最後一次被命令程式訪問的時間,佔據的位元數不同的版本有所不同(如4.0版本佔24位元,2.6版本佔22位元)。

透過對比lru時間與當前時間,可以計算某個物件的空轉時間;object idletime命令可以顯示該空轉時間(單位是秒)。object idletime命令的一個特殊之處在於它不改變物件的lru值。

圖片描述

lru值除了透過object idletime命令列印之外,還與Redis的記憶體回收有關係:如果Redis開啟了maxmemory選項,且記憶體回收演算法選擇的是volatile-lru或allkeys—lru,那麼當Redis記憶體佔用超過maxmemory指定的值時,Redis會優先選擇空轉時間最長的物件進行釋放。

(4)refcount

refcount與共享物件

refcount記錄的是該物件被引用的次數,型別為整型。refcount的作用,主要在於物件的引用計數和記憶體回收。當建立新物件時,refcount初始化為1;當有新程式使用該物件時,refcount加1;當物件不再被一個新程式使用時,refcount減1;當refcount變為0時,物件佔用的記憶體會被釋放。

Redis中被多次使用的物件(refcount>1),稱為共享物件。Redis為了節省記憶體,當有一些物件重複出現時,新的程式不會建立新的物件,而是仍然使用原來的物件。這個被重複使用的物件,就是共享物件。目前共享物件僅支援整數值的字串物件。

共享物件的具體實現

Redis的共享物件目前只支援整數值的字串物件。之所以如此,實際上是對記憶體和CPU(時間)的平衡:共享物件雖然會降低記憶體消耗,但是判斷兩個物件是否相等卻需要消耗額外的時間。對於整數值,判斷操作複雜度為O(1);對於普通字串,判斷複雜度為O(n);而對於雜湊、列表、集合和有序集合,判斷的複雜度為O(n^2)。

雖然共享物件只能是整數值的字串物件,但是5種型別都可能使用共享物件(如雜湊、列表等的元素可以使用)。

就目前的實現來說,Redis伺服器在初始化時,會建立10000個字串物件,值分別是0~9999的整數值;當Redis需要使用值為0~9999的字串物件時,可以直接使用這些共享物件。10000這個數字可以透過調整引數REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值進行改變。

共享物件的引用次數可以透過object refcount命令檢視,如下圖所示。命令執行的結果頁佐證了只有0~9999之間的整數會作為共享物件。

圖片描述

(5)ptr

ptr指標指向具體的資料,如前面的例子中,set hello world,ptr指向包含字串world的SDS。

(6)總結

綜上所述,redisObject的結構與物件型別、編碼、記憶體回收、共享物件都有關係;一個redisObject物件的大小為16位元組:

4bit+4bit+24bit+4Byte+8Byte=16Byte。

Redis沒有直接使用C字串(即以空字元’

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3486/viewspace-2820246/,如需轉載,請註明出處,否則將追究法律責任。

相關文章