本節(jié)介紹五種類(lèi)型的對(duì)象、對(duì)象對(duì)應(yīng)的編碼和相應(yīng)的底層實(shí)現(xiàn)。
一挚赊、對(duì)象的類(lèi)型和編碼
Redis 使用對(duì)象來(lái)表示數(shù)據(jù)庫(kù)中的鍵和值。當(dāng)我們?cè)赗edis中新創(chuàng)建一個(gè)鍵值對(duì)時(shí)捂襟,我們至少創(chuàng)建兩個(gè)對(duì)象咬腕,一個(gè)對(duì)象用作鍵值對(duì)的鍵(鍵對(duì)象)掷漱,另一個(gè)對(duì)象用作鍵值對(duì)的值(值對(duì)象)腊凶。
每一個(gè)對(duì)象都由一個(gè)redisObject結(jié)構(gòu)表示:
typedef struct redisObject { // 類(lèi)型 unsigned type:4; // 編碼 unsigned encoding:4; // 指向底層實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)的指針 void *ptr; // ... } robj;
五種對(duì)象類(lèi)型以及TYPE命令輸出
對(duì)象 | 對(duì)象type屬性的值 | TYPE命令的輸出 |
---|---|---|
字符串對(duì)象 | REDIS_STRING | "string" |
列表對(duì)象 | REDIS_LIST | "list" |
哈希對(duì)象 | REDIS_HASH | "hash" |
集合對(duì)象 | REDIS_SET | set |
有序集合對(duì)象 | REDIS_ZSET | "zset" |
對(duì)象的編碼
encoding屬性記錄了對(duì)象使用的編碼,也即是說(shuō)這個(gè)對(duì)象使用了什么數(shù)據(jù)結(jié)構(gòu)作為對(duì)象的底層實(shí)現(xiàn)抡四。
編碼常量 | 編碼對(duì)應(yīng)的底層數(shù)據(jù)結(jié)構(gòu) |
---|---|
REDIS_ENCODING_INT | long類(lèi)型的整數(shù) |
REDIS_ENCODING_EMBSTR | enbstr編碼的簡(jiǎn)單動(dòng)態(tài)字符串 |
REDIS_ENCODING_RAW | 簡(jiǎn)單動(dòng)態(tài)字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 雙端鏈表 |
REDIS_ENCODING_ZIPLIST | 壓縮列表 |
REDIS_ENCODING_INTSET | 整數(shù)集合 |
REDIS_ENCODING_SKIPLIST | 跳躍表和字典 |
不同類(lèi)型和編碼的對(duì)象
同一對(duì)象類(lèi)型在不同條件下使用不同編碼宠漩,對(duì)應(yīng)不同實(shí)現(xiàn)举反。
類(lèi)型 | 編碼 | 對(duì)象 |
---|---|---|
REDIS_STRING | REDIS_ENCODING_INT | 使用整數(shù)值實(shí)現(xiàn)的字符串對(duì)象 |
REDIS_STRING | REDIS_ENCODING_ENBSTR | 使用embstr編碼的簡(jiǎn)單動(dòng)態(tài)字符串實(shí)現(xiàn)的字符串對(duì)象 |
REDIS_STRING | REDIS_ENCODING_RAW | 使用簡(jiǎn)單動(dòng)態(tài)字符串實(shí)現(xiàn)的字符串的對(duì)象 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實(shí)現(xiàn)的列表對(duì)象 |
REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用雙端鏈表實(shí)現(xiàn)的列表對(duì)象 |
REDIS_HSAH | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實(shí)現(xiàn)的哈希對(duì)象 |
REDIS_HASH | REDIS_ENCODING_HT | 使用字典實(shí)現(xiàn)的哈希對(duì)象 |
REDIS_SET | REDIS_ENCODING_INTSET | 使用整數(shù)集合實(shí)現(xiàn)的集合對(duì)象 |
REDIS_SET | REDIS_ENCODING_HT | 使用字典實(shí)現(xiàn)的集合對(duì)象 |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實(shí)現(xiàn)的有序集合對(duì)象 |
REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用條約表和字典實(shí)現(xiàn)的有序集合對(duì)象 |
使用OBJECT ENCODING命令可以查看一個(gè)數(shù)據(jù)庫(kù)的鍵的值對(duì)象的編碼。
> OBJECT ENCODING key_name
二扒吁、字符串對(duì)象
字符串對(duì)象的編碼可以是int火鼻、raw或者enbstr。
如果一個(gè)字符串對(duì)象保存到的是整數(shù)值雕崩,并且這個(gè)整數(shù)值可以用long類(lèi)型來(lái)表示魁索,那么字符串對(duì)象會(huì)將整數(shù)值保存在字符串對(duì)象結(jié)構(gòu)的ptr屬性里面(將void* 轉(zhuǎn)換成long)并將字符串對(duì)象的編碼設(shè)置為int。
如果字符串對(duì)象保存的是一個(gè)字符串值盼铁,并且字符串長(zhǎng)度大于39字節(jié)粗蔚,那么字符串對(duì)象使用一個(gè)簡(jiǎn)單動(dòng)態(tài)字符串(SDS)來(lái)保存這個(gè)字符串值,并將對(duì)象的編碼設(shè)置為raw饶火。
如果字符串對(duì)象保存的是一個(gè)字符串值鹏控,并且這個(gè)字符串的長(zhǎng)度小于39字節(jié),那么字符串對(duì)象將使用embstr編碼的方式來(lái)保存這個(gè)字符串的值肤寝。
使用embstr編碼的字符串對(duì)象保存短字符串好處:
- 創(chuàng)建字符串對(duì)象所需內(nèi)存分配數(shù)從raw編碼的兩次降低為一次当辐。釋放對(duì)象也只需調(diào)用一次內(nèi)存釋放函數(shù)。
- 所有數(shù)據(jù)都保存在一塊連續(xù)的內(nèi)存里面鲤看,能夠更好地利用緩存帶來(lái)的優(yōu)勢(shì)缘揪。
編碼轉(zhuǎn)換:
- 對(duì)int編碼的字符串執(zhí)行一些命令使這個(gè)對(duì)象報(bào)保存的不再是整數(shù)值,而是一個(gè)字符串值,那么字符串對(duì)象的編碼將從int變成raw寺晌。
- embstr編碼的字符串對(duì)象是只讀的世吨。當(dāng)我們對(duì)embstr編碼的字符串對(duì)象執(zhí)行任何修改命令時(shí),程序會(huì)先將對(duì)象的編碼從embstr轉(zhuǎn)換成raw呻征,然后在執(zhí)行修改命令耘婚。這也是為什么int編碼的字符串對(duì)象進(jìn)行編碼轉(zhuǎn)換只能轉(zhuǎn)換成raw編碼的字符串對(duì)象。
三陆赋、列表對(duì)象
列表對(duì)象的編碼可以是ziplist或者linkedlist沐祷。
linkedlist編碼的列表對(duì)象使用雙端鏈表作為底層實(shí)現(xiàn),每個(gè)雙端鏈表節(jié)點(diǎn)(node)都保存了一個(gè)字符串對(duì)象攒岛,而每個(gè)字符串對(duì)象都保存了一個(gè)列表元素赖临。
這種嵌套字符串對(duì)象的行為在后面介紹的哈希對(duì)象、集合對(duì)象和有序集合對(duì)象中都會(huì)出現(xiàn)灾锯,字符串對(duì)象是redis五種類(lèi)型的對(duì)象唯一一種會(huì)被其他四種對(duì)象嵌套的對(duì)象兢榨。
編碼轉(zhuǎn)換:
同時(shí)滿足以下兩個(gè)條件,列表對(duì)象使用ziplist編碼:
- 列表對(duì)象保存的所有字符串元素長(zhǎng)度都小于64字節(jié)顺饮;
- 列表對(duì)象保存的元素?cái)?shù)量小于512個(gè)吵聪;
- 不能滿足這兩個(gè)條件的列表對(duì)象需要使用linkedlist編碼。
四兼雄、哈希對(duì)象
哈希對(duì)象的編碼可以是ziplist和hashtable吟逝。
ziplist編碼的哈希對(duì)象:
- 先將保存了鍵的壓縮列表節(jié)點(diǎn)推入到壓縮列表表尾,然后再將保存了值的壓縮列表節(jié)點(diǎn)推入到壓縮列表表尾赦肋。兩個(gè)節(jié)點(diǎn)緊挨在一起块攒,鍵節(jié)點(diǎn)在前,值節(jié)點(diǎn)在后佃乘。
- 尾插法囱井。
hashtable編碼的哈希對(duì)象使用字典作為底層實(shí)現(xiàn):
- 字典的每個(gè)鍵都是一個(gè)字符串對(duì)象,對(duì)象中保存了鍵值對(duì)的鍵趣避;
- 字典的每個(gè)值都是一個(gè)字符串對(duì)象琅绅,對(duì)象中保存了鍵值對(duì)的值;
編碼轉(zhuǎn)換:
滿足以下兩個(gè)條件鹅巍,哈希對(duì)象使用ziplist編碼:
- 哈希對(duì)象保存的所有鍵值對(duì)的鍵和值的字符串長(zhǎng)度都小于64字節(jié);
- 哈希對(duì)象保存的鍵值對(duì)數(shù)量小于512個(gè)料祠;
- 不能滿足這兩個(gè)條件的哈希對(duì)象需要使用hashtable編碼骆捧。
五、集合對(duì)象
集合對(duì)象的編碼可以是intset或者h(yuǎn)ashtable髓绽。
編碼轉(zhuǎn)換:
滿足以下兩個(gè)條件敛苇,對(duì)象使用intset編碼:
- 集合對(duì)象保存的所有元素都是整數(shù)值;
- 集合對(duì)象保存的元素?cái)?shù)量不超過(guò)512個(gè);
- 不能滿足這兩個(gè)條件的集合對(duì)象需要使用hashtable編碼枫攀。
六括饶、有序集合對(duì)象
有序集合的編碼可以是ziplist或者skiplist。
skiplist編碼的有序集合對(duì)象使用zset結(jié)構(gòu)作為底層實(shí)現(xiàn)来涨,一個(gè)zset結(jié)構(gòu)同時(shí)包含一個(gè)字典和一個(gè)跳躍表:
typedef struct zset {
zskiplist *zsl;
dict *dict;
} zset;
為什么有序集合需要同時(shí)使用跳躍表和字典來(lái)實(shí)現(xiàn)图焰?
- 使用跳躍表:跳躍表節(jié)點(diǎn)的object屬性保存了元素的成員,而跳躍表節(jié)點(diǎn)的score屬性保存了元素的分值蹦掐。通過(guò)這個(gè)跳躍表技羔,程序可以對(duì)有序集合進(jìn)行范圍型操作,比如ZRANK卧抗、ZRANGE等命令就是基于跳躍表API來(lái)實(shí)現(xiàn)的藤滥。
- 使用字典:程序可以用O(1)復(fù)雜度查找給定成員的分值,ZSCORE命令就是根據(jù)這一特性實(shí)現(xiàn)的社裆。
- 須知:在實(shí)際中拙绊,字典和跳躍表會(huì)共享元素的成員和分值,所以并不會(huì)造成任何數(shù)據(jù)重復(fù)泳秀,也不會(huì)因此浪費(fèi)任何內(nèi)存标沪。
編碼轉(zhuǎn)換:
同時(shí)滿足以下兩個(gè)條件,對(duì)象使用ziplist編碼:
- 有序集合保存的元素?cái)?shù)量小于128個(gè)晶默;
- 有序集合保存的所有元素成員的長(zhǎng)度都小于64字節(jié)谨娜。
- 不滿足以兩個(gè)條件的有序集合對(duì)象都將使用skiplist編碼。
七磺陡、類(lèi)型檢查與命令多態(tài)
Redis用于操作鍵的命令基本可以分為兩種類(lèi)型:
命令可以對(duì)任何類(lèi)型的鍵執(zhí)行趴梢。比如
DEL
命令、EXPIRE
命令币他、RENAME
命令坞靶、TYPE
命令、OBJECT
命令等蝴悉。-
另一種命令只能對(duì)特定類(lèi)型的鍵執(zhí)行彰阴,比如說(shuō):
SET, GET, APPEND, STRLEN等命令只能對(duì)字符串鍵執(zhí)行; ...
類(lèi)型檢查
在執(zhí)行一個(gè)類(lèi)型特定的命令之前拍冠,Redis會(huì)先檢查輸入鍵的類(lèi)型是否正確尿这,然后再?zèng)Q定是否執(zhí)行給定的命令。
類(lèi)型檢查:redisObject.type == target_type ?
多態(tài)命令的實(shí)現(xiàn)
Redis會(huì)根據(jù)值對(duì)象的編碼方式庆杜,選擇正確的命令實(shí)現(xiàn)代碼來(lái)執(zhí)行命令射众。
比如:只要執(zhí)行LLEN命名的是列表鍵,那么無(wú)論值對(duì)象使用的是ziplist還是linkedlist編碼晃财,命令都可以正常實(shí)行叨橱。
DEL、EXPIRE等命令和LLEN等命令的區(qū)別在于,前者是基于類(lèi)型的多態(tài)——一個(gè)命令可以同時(shí)處理不同類(lèi)型的鍵罗洗,而后者是基于編碼的多態(tài)——一個(gè)命令可以同時(shí)處理多種不同編碼愉舔。
八、內(nèi)存回收
因?yàn)镃語(yǔ)言并不具備自動(dòng)內(nèi)存回收功能伙菜,所以Redis在自己的對(duì)象系統(tǒng)中構(gòu)建了一個(gè)引用計(jì)數(shù)(reference counting)技術(shù)實(shí)現(xiàn)的內(nèi)存回收機(jī)制轩缤。
typedef struct redisObject {
// ...
// 引用計(jì)數(shù)
// ...
} robj;
九、對(duì)象共享
除了用于實(shí)現(xiàn)引用計(jì)數(shù)內(nèi)存回收機(jī)制之外仇让,對(duì)象的引用計(jì)數(shù)屬性還帶有對(duì)象共享的作用典奉。
實(shí)現(xiàn)機(jī)制:
- 將數(shù)據(jù)庫(kù)鍵的值指針指向一個(gè)現(xiàn)有的值對(duì)象;
- 將被共享的值對(duì)象的引用計(jì)數(shù)增一丧叽。
共享對(duì)象機(jī)制對(duì)于節(jié)約內(nèi)訓(xùn)非常有幫助卫玖,數(shù)據(jù)庫(kù)中保存的相同值越多,對(duì)象共享機(jī)制就越能節(jié)約越多的內(nèi)存踊淳。
為什么Redis不共享包含字符串的對(duì)象假瞬?
- 一個(gè)共享對(duì)象保存的值越復(fù)雜,驗(yàn)證共享對(duì)象和目標(biāo)對(duì)象是否相同所需的復(fù)雜度就會(huì)越高迂尝,消耗CPU時(shí)間也會(huì)越多脱茉。
- 如果共享對(duì)象是保存整數(shù)值的字符串對(duì)象,那么驗(yàn)證操作的復(fù)雜度為O(1);
- 如果共享對(duì)象是保存字符串值的字符串對(duì)象垄开,那么驗(yàn)證操作的復(fù)雜度為O(N)琴许;
- 受到CPU時(shí)間的限制,Redis只對(duì)包含整數(shù)值的字符串對(duì)象進(jìn)行共享溉躲。
- 目前來(lái)書(shū)榜田,Redis在初始化服務(wù)器時(shí),創(chuàng)建一萬(wàn)個(gè)字符串對(duì)象锻梳,這些對(duì)象包含了從0到9999的所有整數(shù)值箭券,當(dāng)服務(wù)器需要用到值為0到9999的字符串對(duì)象時(shí),服務(wù)器就會(huì)使用這些共享對(duì)象疑枯,而不是新創(chuàng)建對(duì)象辩块。
十、對(duì)象的空轉(zhuǎn)時(shí)長(zhǎng)
除了前面介紹過(guò)的type荆永、encoding废亭、ptr和refcount四個(gè)屬性之外,redisObject結(jié)構(gòu)包含的最后一個(gè)屬性為lru屬性具钥,該屬性記錄了對(duì)象最后一i此被命令程序訪問(wèn)的時(shí)間:
typedef struct redisObject {
// ...
unsigned lru:22;
// ...
}robj;
空轉(zhuǎn)時(shí)長(zhǎng)就是當(dāng)前時(shí)間減去鍵的值對(duì)象的lru時(shí)間計(jì)算得出的豆村。