知識點(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)存