「Redis源碼解讀」—數(shù)據(jù)結(jié)構(gòu)(六)對象

知識點(diǎn)

  • redis數(shù)據(jù)庫中的每一個(gè)鍵值對的鍵和值都是一個(gè)對象
  • redis共有字符串、列表、哈希、集合、有序集合五種類型的對象,每種類型的對象至少都有兩種或以上的編碼方式,不同編碼可以在不同的使用場景上優(yōu)化對象的使用效率
  • redis在執(zhí)行命令之前,會先檢查給定鍵的類型是否能執(zhí)行指定命令,而檢查一個(gè)鍵的類型就是檢查鍵的值對象的類型
  • redis的對象系統(tǒng)帶有引用計(jì)數(shù)實(shí)現(xiàn)的內(nèi)存回收機(jī)制,當(dāng)一個(gè)對象不再被使用時(shí),該對象所占用的內(nèi)存就會被自動釋放
  • redis會共享值為0-9999的字符串對象
  • 對象會記錄自己的最后一次被訪問的時(shí)間,這個(gè)時(shí)間可以用于計(jì)算對象的空轉(zhuǎn)時(shí)間。

結(jié)構(gòu)體

typedef struct redisObject {  
    // 類型  
    unsigned type:4;          
    // 不使用(對齊位)  
    unsigned notused:2;  
    // 編碼方式  
    unsigned encoding:4;  
    // LRU 時(shí)間(相對于 server.lruclock)  
    unsigned lru:22;  
    // 引用計(jì)數(shù)  
    int refcount;  
    // 指向?qū)ο蟮闹? 
    void *ptr;  
} robj;
  • 編碼常量 編碼所對應(yīng)的底層數(shù)據(jù)結(jié)構(gòu)
    REDIS_ENCODING_INT —— long 類型的整數(shù)
    REDIS_ENCODING_EMBSTR —— embstr 編碼的簡單動態(tài)字符串(SDS)
    REDIS_ENCODING_RAW ——簡單動態(tài)字符串
    REDIS_ENCODING_HT ——字典
    REDIS_ENCODING_LINKEDLIST ——雙端鏈表
    REDIS_ENCODING_ZIPLIST ——壓縮列表
    REDIS_ENCODING_INTSET—— 整數(shù)集合
    REDIS_ENCODING_SKIPLIST ——跳躍表

字符串對象

字符串對象的編碼可以是int、raw或者embstr。
如果一個(gè)字符串的內(nèi)容可以轉(zhuǎn)換為long,那么該字符串就會被轉(zhuǎn)換成為long類型,對象的ptr就會指向該long,并且編碼類型也用int類型表示。
普通的字符串有兩種,embstr和raw。embstr編碼是由redIsObject和sdshdr組成,sdshdr結(jié)構(gòu)體如下

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
}

redIsObject占用16字節(jié),如果buf長度是39個(gè)字節(jié),那么sdshdr就是8+39+1=48個(gè)字節(jié),那么embstr就是64字節(jié),而redis采用的是jemalloc內(nèi)存分配器,可以分配8,16,32,64字節(jié)等大小的內(nèi)存,而sdshdr最小分配8+8+1=17字節(jié),那么embstr最小就是33字節(jié),需要分配64字節(jié)。所以對于redis來說小于等于39字節(jié)的字符串采用embstr編碼,大于則用raw編碼。

  • embstr好處:
    1.embstr的創(chuàng)建只需分配一次內(nèi)存,而raw為兩次(一次為sds分配對象,另一次為objet分配對象,embstr省去了第一次,直接分配一塊連續(xù)的內(nèi)存)。
    相對地,釋放內(nèi)存的次數(shù)也由兩次變?yōu)橐淮巍?br> 2.embstr的objet和sds放在一起,更好地利用緩存帶來的優(yōu)勢。
    3.釋放embstr編碼的字符串對象只需要調(diào)用一次內(nèi)存釋放函數(shù),而釋放raw編碼的字符串對象需要條用兩次內(nèi)存釋放函數(shù)
    4.因?yàn)閑mbstr編碼的字符串對象的所有數(shù)據(jù)都保存在一塊連續(xù)的內(nèi)存里面,所有這種編碼的字符串對象比起raw編碼的字符串對象能夠更好的利用緩存帶來的優(yōu)勢。
    需要注意的是,redis并未提供任何修改embstr的方式,即embstr是只讀的形式。對embstr的修改實(shí)際上是先轉(zhuǎn)換為raw再進(jìn)行修改。

列表對象

  • 列表對象的編碼可以是ziplist或者linkedlist。

ziplist是一種壓縮鏈表,它的好處是更能節(jié)省內(nèi)存空間,因?yàn)樗鎯Φ膬?nèi)容都是在連續(xù)的內(nèi)存區(qū)域當(dāng)中的。當(dāng)列表對象元素不大,每個(gè)元素也不大的時(shí)候,就采用ziplist存儲。但當(dāng)數(shù)據(jù)量過大時(shí)就ziplist就不是那么好用了。因?yàn)闉榱吮WC他存儲內(nèi)容在內(nèi)存中的連續(xù)性,插入的復(fù)雜度是O(N),即每次插入都會重新進(jìn)行realloc。

另一方面,linkedlist編碼的列表對象使用雙端鏈表作為底層實(shí)現(xiàn),每個(gè)雙端鏈表節(jié)點(diǎn)Node都保存了一個(gè)字符串對象,而每個(gè)字符串對象都保存了一個(gè)列表元素

編碼轉(zhuǎn)換

當(dāng)列表對象可以同時(shí)滿足以下兩個(gè)條件時(shí),列表對象使用ziplist編碼
1.列表對象保存的所有字符串元素的長度都小于64字節(jié)
2.列表對象保存的元素?cái)?shù)量小于512個(gè),不能滿足這兩個(gè)條件的列表對象需要使用linkedlist編碼

哈希對象

  • 哈希對象的底層實(shí)現(xiàn)可以是ziplist或者h(yuǎn)ashtable。

ziplist中的哈希對象是按照key1,value1,key2,value2這樣的順序存放來存儲的。當(dāng)對象數(shù)目不多且內(nèi)容不大時(shí),這種方式效率是很高的。

編碼轉(zhuǎn)換

當(dāng)哈希對象可以同時(shí)滿足以下兩個(gè)條件時(shí),哈希對象使用ziplist編碼
1.哈希對象保存的所有鍵值對的鍵和值的字符串長度都小于64字節(jié)
2.哈希對象保存的鍵值對數(shù)量小于512個(gè),不能滿足這兩個(gè)條件的哈希對象需要使用hashtable編碼

集合對象

  • 集合對象的編碼可以是intset或者h(yuǎn)ashtable。
    intset是一個(gè)整數(shù)集合,里面存的為某種同一類型的整數(shù),支持如下三種長度的整數(shù):
#define INTSET_ENC_INT16 (sizeof(int16_t))  #define INTSET_ENC_INT32 (sizeof(int32_t))  #define INTSET_ENC_INT64 (sizeof(int64_t))  

intset是一個(gè)有序集合,查找元素的復(fù)雜度為O(logN),但插入時(shí)不一定為O(logN),因?yàn)橛锌赡苌婕暗缴壊僮?。比如?dāng)集合里全是int16_t型的整數(shù),這時(shí)要插入一個(gè)int32_t,那么為了維持集合中數(shù)據(jù)類型的一致,那么所有的數(shù)據(jù)都會被轉(zhuǎn)換成int32_t類型,涉及到內(nèi)存的重新分配,這時(shí)插入的復(fù)雜度就為O(N)了。是intset不支持降級操作。

編碼轉(zhuǎn)換

當(dāng)集合對象可以同時(shí)滿足以下兩個(gè)條件時(shí),集合對象使用intset編碼
1.集合對象保存的所有元素都是整數(shù)值
2.集合對象保存的元素不超過512,不能滿足這兩個(gè)條件的哈希對象需要使用hashtable編碼

有序集合對象

  • 有序集合的編碼可能兩種,一種是ziplist,另一種是skiplist
    ziplist作為集合和作為哈希對象是一樣的,member和score順序存放。按照score從小到大順序排列。它的結(jié)構(gòu)不再復(fù)述。
    skiplist是一種跳躍表,它實(shí)現(xiàn)了有序集合中的快速查找,在大多數(shù)情況下它的速度都可以和平衡樹差不多。但它的實(shí)現(xiàn)比較簡單,可以作為平衡樹的替代品。它的結(jié)構(gòu)比較特殊。
編碼轉(zhuǎn)換

1.有序集合保存的元素?cái)?shù)量小于128個(gè)
2.有序集合保存的所有元素成員的長度都小于64字節(jié)
不能滿足以上兩個(gè)條件的有序集合對象將使用skiplist編碼

內(nèi)存回收

redis在自己的對象系統(tǒng)中構(gòu)建一個(gè)引用計(jì)數(shù)(reference counting)技術(shù)實(shí)現(xiàn)的內(nèi)存回收機(jī)制,通過這一機(jī)制,程序可以通過跟蹤對象的引用計(jì)數(shù)信息,在適當(dāng)?shù)臅r(shí)候自動釋放對象并進(jìn)行內(nèi)存回收。

  • 對象的引用計(jì)數(shù)信息會隨著對象的使用狀態(tài)而不斷變化
    1.在創(chuàng)建一個(gè)新對象時(shí),引用計(jì)數(shù)的值會被初始化為-1;
    2.當(dāng)對象被一個(gè)新程序使用時(shí),它的引用計(jì)數(shù)器會被+1;
    3.當(dāng)對象不再被一個(gè)程序使用時(shí),它的引用計(jì)數(shù)值會被-1;
    4.當(dāng)對象的引用計(jì)數(shù)值變?yōu)?時(shí),對象所占用的內(nèi)存會被釋放;

對象共享

假設(shè)鍵A于鍵B都創(chuàng)建了一個(gè)包含整數(shù)100的字符串對象作為值對象時(shí),redis將會通過讓鍵A和鍵B共享同一個(gè)字符串對象節(jié)約內(nèi)存。

  • 在redis中讓多個(gè)鍵共享一個(gè)值對象需要執(zhí)行以下兩個(gè)步驟:
    1.將數(shù)據(jù)庫鍵的值指針指向一個(gè)現(xiàn)有對象;
    2.將被共享的值對象的引用計(jì)數(shù)器增1;

對象空轉(zhuǎn)時(shí)長

redisObject結(jié)構(gòu)包含的最后一個(gè)屬性為lru屬性,該屬性記錄了對象最后一次被命令程序訪問的時(shí)間;
如果redis打開了maxmemory選項(xiàng),那么當(dāng)redis占用的內(nèi)存數(shù)超過了maxmemory選項(xiàng)所設(shè)置的上限時(shí),空轉(zhuǎn)時(shí)長較高的那部分鍵會被優(yōu)先被釋放,從而回收內(nèi)存

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Redis的內(nèi)存優(yōu)化 聲明:本文內(nèi)容來自《Redis開發(fā)與運(yùn)維》一書第八章,如轉(zhuǎn)載請聲明。 Redis所有的數(shù)據(jù)都...
    meng_philip123閱讀 19,060評論 2 29
  • 參考來源 Redis的內(nèi)存優(yōu)化 Redis所有的數(shù)據(jù)都在內(nèi)存中,而內(nèi)存又是非常寶貴的資源。對于如何優(yōu)化內(nèi)存使用一直...
    秦漢郵俠閱讀 1,369評論 0 2
  • 轉(zhuǎn)載:可能是目前最詳細(xì)的Redis內(nèi)存模型及應(yīng)用解讀 Redis是目前最火爆的內(nèi)存數(shù)據(jù)庫之一,通過在內(nèi)存中讀寫數(shù)據(jù)...
    meng_philip123閱讀 1,507評論 1 22
  • 轉(zhuǎn)載:可能是目前最詳細(xì)的Redis內(nèi)存模型及應(yīng)用解讀 Redis是目前最火爆的內(nèi)存數(shù)據(jù)庫之一,通過在內(nèi)存中讀寫數(shù)據(jù)...
    jwnba24閱讀 683評論 0 4
  • 高考與上海這座國際化大都市失之交臂,去了江蘇。大學(xué)時(shí)就立下志向畢業(yè)去上海工作,10年校園招聘只投上海企業(yè)簡歷而且H...
    DennisFly閱讀 2,998評論 1 0

友情鏈接更多精彩內(nèi)容