使用對象的好處:
- 在執(zhí)行命令之前,根據(jù)對象的類型來判斷一個對象是否可以執(zhí)行給定的命令叽讳。
- 2.可以針對不同的使用場景寡喝,為對象設(shè)置多種不同的數(shù)據(jù)結(jié)構(gòu)實現(xiàn),從而優(yōu)化對象在不同場景下的使用效率历造。
- 3.實現(xiàn)了基于引用計數(shù)技術(shù)的內(nèi)存回收機制。
- 4.通過引用計數(shù)技術(shù)實現(xiàn)了對象共享機制,在適當(dāng)?shù)臈l件下帕膜,通過讓多個數(shù)據(jù)庫鍵來共享同一個對象來節(jié)約內(nèi)存枣氧。
- 5.redis的對象帶有訪問時間記錄信息溢十,可以用于計算數(shù)據(jù)庫鍵的空轉(zhuǎn)時間垮刹。
1. 五種對象
字符串對象,列表對象张弛,哈希對象荒典,集合對象,有序集合對象吞鸭。
typedef struct redisObject
{
// 類型
unsigned type:4;
// 編碼
unsigned encoding:4;
// 指向底層實現(xiàn)數(shù)據(jù)結(jié)構(gòu)的指針
void *ptr;
} robj;
2 字符串對象(REDIS_STRING)
采用的編碼:REDIS_ENCODING_INT, REDIS_ENCODING_EMBSTR, REDIS_ENCODING_RAW
一個字符串對象保存的是整數(shù)值寺董,且可以用long類型來表示,那么編碼為int刻剥。如果保存的是一個字符串值遮咖,且長度大于39字節(jié),那么編碼為raw造虏。小于等于39字節(jié)御吞,編碼采用embstr。
2.1 raw和embstr的區(qū)別
raw和embstr都是使用redisobject結(jié)構(gòu)和sdshdr結(jié)構(gòu)(即SDS)來表示字符串對象漓藕,但raw編碼會調(diào)用兩次內(nèi)存分配函數(shù)來分別創(chuàng)建redisobject和sdshdr陶珠,而embstr編碼通過調(diào)用一次內(nèi)存分配函數(shù)來分配一塊連續(xù)的空間,空間中依次包含redisobject和sdshdr兩個結(jié)構(gòu)享钞。
用embstr編碼的字符串對象來保存短字符串值有以下好處:
- embstr編碼將創(chuàng)建字符串對象所需的內(nèi)存分配次數(shù)從raw編碼的兩次降為一次揍诽。
- 釋放embstr編碼的字符串對象只需要調(diào)用一次內(nèi)存釋放函數(shù),而釋放raw編碼的需要兩次栗竖。
- 因為embstr編碼的字符串對象的所有數(shù)據(jù)都是保存在一塊連續(xù)的內(nèi)存里面暑脆,所以這種編碼的字符串對象比起raw編碼的字符串對象能夠更好的利用緩存帶來的優(yōu)勢。
2.2 編碼的轉(zhuǎn)換
int編碼的字符串對象狐肢,如果對對象執(zhí)行了一些命令添吗,使得它不再是整數(shù)值,那編碼就會從int變成raw处坪。
embstr編碼的字符串對象是只讀的根资。如果對embstr編碼的字符串對象做修改,那程序會先把對象的編碼從embstr轉(zhuǎn)換成raw同窘,然后再執(zhí)行修改命令玄帕。所以embstr的字符串對象執(zhí)行修改命令后,一定會變成一個raw編碼的字符串對象想邦。
3 列表對象(REDIS_LIST)
就是普通的list
采用的編碼:REDIS_ENCODING_ZIPLIST, REDIS_ENCODING_LINKEDLIST
代表命令:RPUSH,LPUSH,RPOP,LPOP,LLEN
ziplist編碼的列表對象使用壓縮列表作為底層實現(xiàn)裤纹。linkedlist使用雙端鏈表作為底層實現(xiàn)。
3.1 編碼轉(zhuǎn)換
- 列表對象保存的所有字符串對象元素的長度都小于64字節(jié);
- 列表對象保存的元素數(shù)量小于512個鹰椒。
當(dāng)列表對象可以同時滿足這上面兩個條件時锡移,列表對象使用ziplist編碼。否則漆际,使用linkedlist編碼淆珊。
4 哈希對象(REDIS_HASH)
就是類似于Java的map
采用的編碼:REDIS_ENCODING_ZIPLIST, REDIS_ENCODING_HT(hashtable)
當(dāng)采用了ziplist來保存編碼的時候,保存鍵的節(jié)點和保存值的節(jié)點一前一后在一起奸汇。
代表命令:HSET, HGET, HLEN
4.1 編碼轉(zhuǎn)換
- 哈希對象保存的所有鍵值對的鍵和值的字符串對象元素的長度都小于64字節(jié)施符;
- 哈希對象保存的元素數(shù)量小于512個。
當(dāng)哈希對象可以同時滿足這上面兩個條件時擂找,哈希對象使用ziplist編碼戳吝。否則,使用hashtable編碼贯涎。
5 集合對象(REDIS_SET)
類似于java的set
采用的編碼:REDIS_ENCODING_INTSET, REDIS_ENCODING_HT
代表命令:SADD, SPOP,SCARD
5.1 編碼轉(zhuǎn)換
- 集合對象保存的所有元素都是整數(shù)值听哭;
- 集合對象保存的元素數(shù)量小于512個。
當(dāng)集合對象可以同時滿足這上面兩個條件時塘雳,集合對象使用intset編碼陆盘。否則,使用hashtable編碼粉捻。
6 有序集合對象(REDIS_ZSET)
帶有分值(score)的set礁遣,在保存上是從小到大,有序的肩刃。
采用的編碼:REDIS_ENCODING_ZIPLIST, REDIS_ENCODING_SKIPLIST
當(dāng)采用了ziplist來保存編碼的時候祟霍,每個集合元素使用兩個緊挨在一起的壓縮列表節(jié)點來保存,第一個保存元素的成員盈包,而第二個元素則保存元素的分值沸呐。
代表命令:ZADD, ZCOUNT,ZCARD
6.1 skiplist編碼的有序集合對象實現(xiàn)
skiplist編碼的有序集合對象使用zset結(jié)構(gòu)作為底層實現(xiàn),一個zset結(jié)構(gòu)同時包含一個字典和一個跳躍表:
typedef struct zset
{
zskiplist *zsl;
dict *dict
} zset;
跳躍表和字典同時來保存有序集合元素呢燥,但這兩種數(shù)據(jù)結(jié)構(gòu)會通過指針來共享相同元素的成員和分值崭添。所以不會產(chǎn)生任何重復(fù)成員或者分值,也不會因此浪費內(nèi)存叛氨。
why?
因為使用字典呼渣,我們可以以O(shè)(1)復(fù)雜度查找成員的分值,但字典是無序的寞埠。使用跳躍表執(zhí)行范圍型操作的所有優(yōu)點就會保留下來屁置。所以redis采用兩種同時來實現(xiàn),可以讓有序集合的查找和范圍型操作都盡可能快的執(zhí)行仁连。
6.2 編碼轉(zhuǎn)換
- 有序集合對象保存的所有元素成員的長度都小于64字節(jié)蓝角;
- 有序集合對象保存的元素數(shù)量小于128個。
當(dāng)有序集合對象可以同時滿足這上面兩個條件時,有序集合對象使用ziplist編碼使鹅。否則揪阶,使用skiplist編碼。
7. 類型檢查與命令多態(tài)
redis用于操作鍵的命令基本可以分為兩種類型患朱,一種可以對任何類型的鍵執(zhí)行鲁僚,另一種只能對特定類型的鍵執(zhí)行。
7.1 類型檢查的實現(xiàn)
類型檢查通過redisobject的type屬性來實現(xiàn):
- 在執(zhí)行一個特定類型的命令之前麦乞,服務(wù)器會先檢查輸入數(shù)據(jù)庫鍵的值對象是否為執(zhí)行命令所需類型蕴茴,如果是執(zhí)行劝评;
- 否則姐直,拒絕執(zhí)行,并向客戶端返回一個類型錯誤蒋畜。
7.2 多態(tài)命令的實現(xiàn)
redis還會根據(jù)值對象的編碼方式声畏,選擇正確的命令實現(xiàn)代碼來執(zhí)行命令。
舉個例子:比如列表對象有ziplist和linkedlist兩種編碼可用姻成,前者使用壓縮列表api來實現(xiàn)列表命令插龄,后者使用雙端鏈表api來實現(xiàn)。這就是多態(tài)科展,只要執(zhí)行的是某個類型均牢,無論使用哪種編碼都可以正常執(zhí)行。
DEL, EXPIRE等命令也是多態(tài)才睹,基于類型的多態(tài)徘跪,一個命令可以同時處理多種不同類型的鍵;而LLEN等命令琅攘,基于編碼的多態(tài)垮庐,一個命令可以同時處理多種不同編碼。
8.內(nèi)存回收
引用計數(shù)技術(shù)實現(xiàn)的內(nèi)存回收機制坞琴。對象的整個生命周期可以分為創(chuàng)建對象哨查,操作對象,釋放對象三個階段剧辐。
typedef struct redisobject
{
//...
// 引用計數(shù)
int refcount;
//...
} robj;
- incrRefCount:將對象的引用計數(shù)值+1
- decrRefCount:將對象的引用計數(shù)值-1寒亥,當(dāng)對象的引用計數(shù)值為0時,釋放對象荧关。
- resetRefCount:將對象的引用計數(shù)值設(shè)置為0溉奕,但不釋放對象,這個函數(shù)通常在需要重新設(shè)置對象的引用計數(shù)值時使用羞酗。
9.對象共享
在redis中腐宋,讓多個鍵共享同一個值對象需要執(zhí)行以下兩個步驟:
- 1.將數(shù)據(jù)庫鍵的指針指向一個現(xiàn)有的值對象;
- 2.將被共享的值對象引用計數(shù)+1.
- redis只對包含整數(shù)值的字符串對象進行共享(因為需要驗證是否相同,耗費cpu時間)胸竞。
- 目前在初始化服務(wù)器時欺嗤,會創(chuàng)建一萬個字符串對象,包含了從0到9999的所有整數(shù)值卫枝。
10. 空轉(zhuǎn)時間
typedef struct redisobject
{
//...
// 記錄了對象最后一次被命令程序訪問的時間
unsigned lru:22;
//...
} robj;
OBJECT IDLETIME 命令可以打印出給定鍵的空轉(zhuǎn)時長煎饼。這個命令在訪問鍵的值對象時,不會修改值對象的lru屬性校赤。
鍵的空轉(zhuǎn)時長還有一個作用:如果服務(wù)器打開了maxmemory選項吆玖,且回收算法是空轉(zhuǎn)時長,那么久利用這個屬性來回收內(nèi)存马篮,釋放空間沾乘。