關(guān)于Redis數(shù)據(jù)存儲的細(xì)節(jié),涉及到內(nèi)存分配器(如jemalloc)、簡單動態(tài)字符串(SDS)、5種對象類型及內(nèi)部編碼、redisObject
這里將說明這幾個概念之間的關(guān)系。
下圖是執(zhí)行set hello world時,所涉及到的數(shù)據(jù)模型:
- dictEntry:Redis是Key-Value數(shù)據(jù)庫,因此對每個鍵值對都會有一個dictEntry,里面存儲了指向Key和Value的指針;next指向下一個dictEntry,與本Key-Value無關(guān)。
- Key:圖中右上角可見,Key(”hello”)并不是直接以字符串存儲,而是存儲在SDS結(jié)構(gòu)中。
- redisObject:Value(“world”) 既不是直接以字符串存儲,也不是像Key一樣直接存儲在SDS中,而是存儲在redisObject中。實際上,不論Value是5種類型的哪一種,都是通過redisObject來存儲的;而redisObject中的type字段指明了Value對象的類型,ptr字段則指向?qū)ο笏诘牡刂?。不過可以看出,字符串對象雖然經(jīng)過了redisObject的包裝,但仍然需要通過SDS存儲。實際上,redisObject除了type和ptr字段以外,還有其他字段圖中沒有給出,如用于指定對象內(nèi)部編碼的字段
- jemalloc:無論是DictEntry對象,還是redisObject、SDS對象,都需要內(nèi)存分配器(如jemalloc)分配內(nèi)存進(jìn)行存儲。以DictEntry對象為例,有3個指針組成,在64位機(jī)器下占24個字節(jié),jemalloc會為它分配32字節(jié)大小的內(nèi)存單元。
SDS--簡單動態(tài)字符串
- (1) SDS結(jié)構(gòu)
struct sdsstr{
// buf已使用的長度
int len;
// 示buf未使用的長度
int free;
// 字節(jié)數(shù)組,用來存儲字符串
char buf[];
};
通過SDS的結(jié)構(gòu)可以看出,buf數(shù)組的長度=free+len+1(其中1表示字符串結(jié)尾的空字符)
所以,一個SDS結(jié)構(gòu)占據(jù)的空間為:free所占長度+len所占長度+ buf數(shù)組的長度=4+4+free+len+1=free+len+9
-
(2) SDS與C字符串的比較
SDS在C字符串的基礎(chǔ)上加入了free和len字段,帶來了很多好處:
- 獲取字符串長度:SDS是O(1),C字符串是O(n)
- 緩沖區(qū)溢出:使用C字符串的API時,如果字符串長度增加(如strcat操作)而忘記重新分配內(nèi)存,很容易造成緩沖區(qū)的溢出;而SDS由于記錄了長度,相應(yīng)的API在可能造成緩沖區(qū)溢出時會自動重新分配內(nèi)存,杜絕了緩沖區(qū)溢出。
- 修改字符串時內(nèi)存的重分配:對于C字符串,如果要修改字符串,必須要重新分配內(nèi)存(先釋放再申請),因為如果沒有重新分配,字符串長度增大時會造成內(nèi)存緩沖區(qū)溢出,字符串長度減小時會造成內(nèi)存泄露。而對于SDS,由于可以記錄len和free,因此解除了字符串長度和空間數(shù)組長度之間的關(guān)聯(lián),可以在此基礎(chǔ)上進(jìn)行優(yōu)化:空間預(yù)分配策略(即分配內(nèi)存時比實際需要的多)使得字符串長度增大時重新分配內(nèi)存的概率大大減??;惰性空間釋放策略使得字符串長度減小時重新分配內(nèi)存的概率大大減小。
- 存取二進(jìn)制數(shù)據(jù):SDS可以,C字符串不可以。因為C字符串以空字符作為字符串結(jié)束的標(biāo)識,而對于一些二進(jìn)制文件(如圖片等),內(nèi)容可能包括空字符串,因此C字符串無法正確存??;而SDS以字符串長度len來作為字符串結(jié)束標(biāo)識,因此沒有這個問題。
此外,由于SDS中的buf仍然使用了C字符串(即以’\0’結(jié)尾),因此SDS可以使用C字符串庫中的部分函數(shù);但是需要注意的是,只有當(dāng)SDS用來存儲文本數(shù)據(jù)時才可以這樣使用,在存儲二進(jìn)制數(shù)據(jù)時則不行(’\0’不一定是結(jié)尾)
-
(3) SDS與C字符串的應(yīng)用
Redis在存儲對象時,一律使用SDS代替C字符串。
例如set hello world命令,hello和world都是以SDS的形式存儲的。
而sadd myset member1 member2 member3命令,不論是鍵(”myset”),還是集合中的元素 (”member1”、 ”member2”和”member3”),都是以SDS的形式存儲。 除了存儲對象,SDS還用于存儲各種緩沖區(qū)。
只有在字符串不會改變的情況下,如打印日志時,才會使用C字符串。
jemalloc
Redis在編譯時便會指定內(nèi)存分配器;內(nèi)存分配器可以是 libc 、jemalloc或者tcmalloc,默認(rèn)是 jemalloc。
jemalloc作為Redis的默認(rèn)內(nèi)存分配器,在減小內(nèi)存碎片方面做的相對比較好。jemalloc在64位系統(tǒng)中,將內(nèi)存空間劃分為小、大、巨大三個范圍;每個范圍內(nèi)又劃分了許多小的內(nèi)存塊單位;當(dāng)Redis存儲數(shù)據(jù)時,會選擇大小最合適的內(nèi)存塊進(jìn)行存儲。
jemalloc劃分的內(nèi)存單元如下圖所示:
例如,如果需要存儲大小為130
這里是引用字節(jié)的對象,jemalloc會將其放入160字節(jié)的內(nèi)存單元中
redisObject
Redis對象有
5種類型;無論是哪種類型,Redis都不會直接存儲,而是通過redisObject對象進(jìn)行存儲。
redisObject對象非常重要,Redis對象的類型、內(nèi)部編碼、內(nèi)存回收、共享對象等功能,都需要redisObject支持,下面將通過redisObject的結(jié)構(gòu)來說明它是如何起作用的。
redisObject的定義如下(列出了與保存數(shù)據(jù)有關(guān)的三個屬性):
typedef struct redisObject{
unsigned type: 4;
unsigned encoding: 4;
unsigned lru: REDIS_LRU_BITS;
int refcount;
void *ptr;
} robj
-
(1) type
type字段表示對象的類型,占4個比特;
目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
當(dāng)我們執(zhí)行type命令時,便是通過讀取RedisObject的type字段獲得對象的類型;
127.0.0.1:6379> set test hello_redis
OK
127.0.0.1:6379> type test
string
127.0.0.1:6379> sadd myset member1 member2 member3
(integer) 3
127.0.0.1:6379> type myset
set
-
(2) encoding
encoding表示對象的內(nèi)部編碼,占4個比特。
對于Redis支持的每種類型,都有至少兩種內(nèi)部編碼,例如對于字符串,有int、embstr、raw三種編碼。
通過encoding屬性,Redis可以根據(jù)不同的使用場景來為對象設(shè)置不同的編碼,大大提高了Redis的靈活性和效率。
以列表對象為例,有壓縮列表和雙端鏈表兩種編碼方式;如果列表中的元素較少,Redis傾向于使用壓縮列表進(jìn)行存儲,因為壓縮列表占用內(nèi)存更少,而且比雙端鏈表可以更快載入;當(dāng)列表對象元素較多時,壓縮列表就會轉(zhuǎn)化為更適合存儲大量元素的雙端鏈表。
通過object encoding命令,可以查看對象采用的編碼方式:
127.0.0.1:6379> set key1 123
OK
127.0.0.1:6379> object encoding key1
"int"
127.0.0.1:6379> set key1 helloredis
OK
127.0.0.1:6379> object encoding key1
"embstr"
-
(3) lru
lru記錄的是對象最后一次被命令程序訪問的時間,占據(jù)的比特數(shù)不同的版本有所不同(如4.0版本占24bit,2.6版本占22bit)
通過對比lru時間與當(dāng)前時間,可以計算某個對象的閑置時間;object idletime命令可以顯示該閑置時間(單位是秒)。object idletime命令的一個特殊之處在于它不改變對象的lru值。
127.0.0.1:6379> set key1 helloredis
OK
127.0.0.1:6379> object idletime key1
(integer) 13
127.0.0.1:6379> object idletime key1
(integer) 37
127.0.0.1:6379> object idletime key1
(integer) 40
127.0.0.1:6379> object idletime key1
(integer) 43
lru值除了通過object idletime命令打印之外,還與Redis的內(nèi)存回收有關(guān)系:
如果Redis打開了maxmemory選項,且內(nèi)存回收算法選擇的是volatile-lru或allkeys—lru,那么當(dāng)Redis內(nèi)存占用超過maxmemory指定的值時,Redis會優(yōu)先選擇空轉(zhuǎn)時間最長的對象進(jìn)行釋放。
- (4) refcount
refcount與共享對象
refcount記錄的是該對象被引用的次數(shù),類型為整型。refcount的作用,主要在于對象的引用計數(shù)和內(nèi)存回收。當(dāng)創(chuàng)建新對象時,refcount初始化為1;當(dāng)有新程序使用該對象時,refcount加1;當(dāng)對象不再被一個新程序使用時,refcount減1;當(dāng)refcount變?yōu)?時,對象占用的內(nèi)存會被釋放。
Redis中被多次使用的對象(refcount>1),稱為共享對象。 Redis為了節(jié)省內(nèi)存,當(dāng)有一些對象重復(fù)出現(xiàn)時,新的程序不會創(chuàng)建新的對象,而是仍然使用原來的對象。這個被重復(fù)使用的對象,就是共享對象。目前共享對象僅支持整數(shù)值的字符串對象。
共享對象的具體實現(xiàn)
Redis的共享對象目前只支持整數(shù)值的字符串對象。之所以如此,實際上是對內(nèi)存和CPU(時間)的平衡:共享對象雖然會降低內(nèi)存消耗,但是判斷兩個對象是否相等卻需要消耗額外的時間。對于整數(shù)值,判斷操作復(fù)雜度為O(1);對于普通字符串,判斷復(fù)雜度為O(n);而對于哈希、列表、集合和有序集合,判斷的復(fù)雜度為O(n^2)。
雖然共享對象只能是整數(shù)值的字符串對象,但是5種類型都可能使用共享對象(如哈希、列表等的元素可以使用)。
就目前的實現(xiàn)來說,Redis服務(wù)器在初始化時,會創(chuàng)建10000個字符串對象,值分別是0 ~ 9999的整數(shù)值;當(dāng)Redis需要使用值為0 ~ 9999的字符串對象時,可以直接使用這些共享對象。10000這個數(shù)字可以通過調(diào)整參數(shù)REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值進(jìn)行改變。共享對象的引用次數(shù)可以通過object refcount命令查看,如下圖所示。命令執(zhí)行的結(jié)果頁佐證了只有0~9999之間的整數(shù)會作為共享對象。
127.0.0.1:6379> set k1 9999
OK
127.0.0.1:6379> set k2 9999
OK
127.0.0.1:6379> set k3 9999
OK
127.0.0.1:6379> object refcount k1
(integer) 2147483647
127.0.0.1:6379> set k1 10000
OK
127.0.0.1:6379> set k2 10000
OK
127.0.0.1:6379> set k3 10000
OK
127.0.0.1:6379> object refcount k1
(integer) 1
127.0.0.1:6379> set k1 hello
OK
127.0.0.1:6379> set k2 hello
OK
127.0.0.1:6379> set k3 hello
OK
127.0.0.1:6379> object refcount k1
(integer) 1
- (5) ptr
ptr指針指向具體的數(shù)據(jù),如前面的例子中,set hello world,ptr指向包含字符串world的SDS。
綜上所述,redisObject的結(jié)構(gòu)與對象類型、編碼、內(nèi)存回收、共享對象都有關(guān)系;一個redisObject對象的大小為16字節(jié):
4bit+4bit+24bit+4Byte+8Byte=16Byte