歡迎下載開源中國APP獲取更多優質文章
前言
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命令可以查看內存使用情況:
1
info 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的內存佔用主要可以劃分為以下幾個部分:
1、數據
作為資料庫,數據是最主要的部分;這部分佔用的內存會統計在used_memory中。
Redis使用鍵值對存儲數據,其中的值(對象)包括5種類型,即字元串、哈希、列表、集合、有序集合。這5種類型是Redis對外提供的,實際上,在Redis內部,每種類型可能有2種或更多的內部編碼實現;此外,Redis在存儲對象時,並不是直接將數據扔進內存,而是會對對象進行各種包裝:如redisObject、SDS等;這篇文章後面將重點介紹Redis中數據存儲的細節。
2、進程本身運行需要的內存
Redis主進程本身運行肯定需要佔用內存,如代碼、常量池等等;這部分內存大約幾兆,在大多數生產環境中與Redis數據佔用的內存相比可以忽略。這部分內存不是由jemalloc分配,因此不會統計在used_memory中。
補充說明:除了主進程外,Redis創建的子進程運行也會佔用內存,如Redis執行AOF、RDB重寫時創建的子進程。當然,這部分內存不屬於Redis進程,也不會統計在used_memory和used_memory_rss中。
3、緩衝內存
緩衝內存包括客戶端緩衝區、複製積壓緩衝區、AOF緩衝區等;其中,客戶端緩衝存儲客戶端連接的輸入輸出緩衝;複製積壓緩衝用於部分複製功能;AOF緩衝區用於在進行AOF重寫時,保存最近的寫入命令。在了解相應功能之前,不需要知道這些緩衝的細節;這部分內存由jemalloc分配,因此會統計在used_memory中。
4、內存碎片
內存碎片是Redis在分配、回收物理內存過程中產生的。例如,如果對數據的更改頻繁,而且數據之間的大小相差很大,可能導致redis釋放的空間在物理內存中並沒有釋放,但redis又無法有效利用,這就形成了內存碎片。內存碎片不會統計在used_memory中。
內存碎片的產生與對數據進行的操作、數據的特點等都有關;此外,與使用的內存分配器也有關係:如果內存分配器設計合理,可以儘可能的減少內存碎片的產生。後面將要說到的jemalloc便在控制內存碎片方面做的很好。
如果Redis伺服器中的內存碎片已經很大,可以通過安全重啟的方式減小內存碎片:因為重啟之後,Redis重新從備份文件中讀取數據,在內存中進行重排,為每個數據重新選擇合適的內存單元,減小內存碎片。
1、概述
關於Redis數據存儲的細節,涉及到內存分配器(如jemalloc)、簡單動態字元串(SDS)、5種對象類型及內部編碼、redisObject。在講述具體內容之前,先說明一下這幾個概念之間的關係。
下圖是執行set hello world時,所涉及到的數據模型。
圖片來源:https://searchdatabase.techtarget.com.cn/7-20218/
(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、對象類型及內部編碼。
2、jemalloc
Redis在編譯時便會指定內存分配器;內存分配器可以是 libc 、jemalloc或者tcmalloc,默認是jemalloc。
jemalloc作為Redis的默認內存分配器,在減小內存碎片方面做的相對比較好。jemalloc在64位系統中,將內存空間劃分為小、大、巨大三個範圍;每個範圍內又劃分了許多小的內存塊單位;當Redis存儲數據時,會選擇大小最合適的內存塊進行存儲。
jemalloc劃分的內存單元如下圖所示:
圖片來源:http://blog.csdn.net/zhengpeitao/article/details/76573053
例如,如果需要存儲大小為130位元組的對象,jemalloc會將其放入160位元組的內存單元中。
3、redisObject
前面說到,Redis對象有5種類型;無論是哪種類型,Redis都不會直接存儲,而是通過redisObject對象進行存儲。
redisObject對象非常重要,Redis對象的類型、內部編碼、內存回收、共享對象等功能,都需要redisObject支持,下面將通過redisObject的結構來說明它是如何起作用的。
redisObject的定義如下(不同版本的Redis可能稍稍有所不同):
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)。進群619881427可以免費獲取文中知識點的視頻資料。
雖然共享對象只能是整數值的字元串對象,但是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。
4、SDS
Redis沒有直接使用C字元串(即以空字元』