Redis主要數據結構:SDS、雙端鏈表、字典、壓縮列表、整數集合等等。node
Redis並無直接使用這些數據結構來實現鍵值對數據庫,而是基於這些數據結構建立了一個對象系統,這個系統包含字符串對象、列表對象、哈希對象、集合對象和有序集合對象這 5 種類型的對象。每種對象都至少用到了上述所說的數據結構。redis
經過這5 種不一樣類型的對象,Redis能夠在執行命令前,根據對象的類型來判斷對象是否能夠執行給定的命令。使用對象的另外一個好處就是能夠針對不一樣的使用場景爲對象設置不一樣的數據結構實現,從而優化對象在不一樣場景下的使用效率。算法
除此以外,Redis的對象系統還實現了基於引用計數計數的內存回收機制,當程序再也不使用某個對象的時候,這個對象所佔用的內存就會被自動是否;另外,Redis還經過引用計數技術實現了對象共享機制,這一機制能夠在適當的條件下,經過讓多個數據庫共享同一個對象來節約內存。sql
最後,Redis的對象帶有訪問時間記錄信息,該信息能夠用於計算數據庫鍵的空轉時長,在服務器啓用了maxmemory功能的狀況下,空轉時長越大的那些鍵就可能會優先被服務器刪除。數據庫
對象的類型與編碼:數組
Redis使用對象表示數據庫中的鍵和值,Redis每次在數據庫中建立一個鍵值對時,至少會建立兩個對象:一個對象用做鍵值對中的鍵(鍵對象),一個用做鍵值對中的值(值對象)。緩存
Redis中的每一個對象都由一個redisObject結構表示,該結構中保存有與數據相關的三個屬性:服務器
typedef struct redisObject{ //類型 unsigned type:4; //編碼 unsigned encoding:4; //指向底層實現數據結構的指針 void *ptr; }robj;
類型:數據結構
type屬性指向的對象的類型:app
類型常量 | 對象的名稱 | TYPE命令輸出 |
REDIS_STRING | 字符串對象 | "string" |
REDIS_LIST | 列表對象 | "list" |
REDIS_HASH | 哈希對象 | "hash" |
REDIS_SET | 集合對象 | "set" |
REDIS_ZSET | 有序集合對象 | "zset" |
對於Redis數據庫保存的鍵值對來講,鍵老是一個字符串對象,而值能夠是字符串對象,列表對象,哈希對象,集合對象或者有序集合對象中的其中一種。
TYPE命令的實現方式:當對一個數據庫鍵執行TYPE命令時,命令返回的結果爲數據庫鍵對應的值的對象類型,而不是鍵對象的類型。
編碼和底層實現:
對象的ptr指針指向對象的底層實現數據結構,而這些數據結構由對象的encoding屬性決定。
encoding屬性記錄了對象所使用的編碼,也就是說這個對象使用了什麼數據結構做爲對象的底層實現。這個屬性的值能夠是如下列表列出的常量的其中一個:
對象的編碼 | |
編碼常量 | 編碼所對應的底層數據結構 |
REDIS_ENCODING_INT | long類型的整數 |
REDIS_ENCODING_EMBSTR | embstr編碼的簡單動態字符串 |
REDIS_ENCODING_RAW | 簡單動態字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 雙端鏈表 |
REDIS_ENCODING_ZIPLIST | 壓縮列表 |
REDIS_ENCODING_INTSET | 整數集合 |
REDIS_ENCODING_SKIPLIST | 跳躍表和字典 |
上述說明的是不一樣對象的編碼所對應的底層數據結構,並且每種類型的對象都至少使用了兩種不一樣的編碼,下表列出了每種類型的對象可使用的編碼:
不一樣類型和編碼的對象 | ||
類型 | 編碼 | 對象 |
REDIS_STRING |
REDIS_ENCODING_INT | 使用整數值實現的字符串對象 |
REDIS_ENCODING_EMBSTR | 使用embstr編碼的簡單動態字符串實現的字符串對象 | |
REDIS_ENCODING_RAW | 使用簡單動態字符串實現的字符串對象 | |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實現的列表對象 |
REDIS_ENCODING_LINKEDLIST | 使用雙端鏈表實現的列表對象 | |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實現的列表對象 |
REDIS_ENCODING_HT | 使用字典實現的哈希對象 | |
REDIS_SET | REDIS_ENCODING_INTSET | 使用整數集合實現的集合對象 |
REDIS_ENCODING_HT | 使用字典實現的集合對象 | |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實現的有序集合對象 |
REDIS_ENCODING_SKIPLIST | 使用跳躍表和字典實現的有序集合對象 |
使用OBEJCT ENCODING命令能夠查看一個數據庫鍵的值對象的編碼:
下表列出了不一樣編碼對象所對應的OBJECT ENCODING命令輸出:
OBJECT ENCODING命令輸出 | ||
對象所使用的底層數據結構 | 編碼常量 | OBJECT ENCODING命令輸出 |
整數 | REDIS_ENCODING_INT | "int" |
embstr編碼的簡單動態字符串(SDS) | REDIS_ENCODING_EMBSTR | "embstr" |
簡單動態字符串 | REDIS_ENCODING_RAW | "raw" |
字典 | REDIS_ENCODING_HT | "hashtable" |
雙端鏈表 | REDIS_ENCODING_LINKEDLIST | "linkedlist" |
壓縮鏈表 | REDIS_ENCODING_ZIPLIST | "ziplist" |
整數集合 | REDIS_ENCODING_INTSET | "intset" |
跳躍表和字典 | REDIS_ENCODING_SKIPLIST | "skiplist" |
經過encoding屬性來設定對象所使用的編碼,而不是爲特定類型的對象關聯一種特定的編碼,極大的提高了Redis的靈活性和效率,由於Redis能夠根據不一樣的使用場景爲一個對象設置不一樣的編碼,從而優化對象在某一場景下的使用。
字符串對象:
字符串對象的編碼能夠是int、raw或者embstr。
1. 若是一個字符串對象保存的是整數值,而且這個整數值能夠用long類型來表示,那麼字符串對象會將整數值保存在字符串對象結構的ptr屬性裏面(將void*轉換成long),並將字符串對象的編碼設置爲int。
2. 若是一個字符串對象保存的是一個字符串值,而且這個字符串值的長度大於39字節,那麼字符串對象將使用一個簡單動態字符串(SDS)來保存這個字符串值,並將對象的編碼設置爲raw。
3. 若是一個字符串對象保存的是一個字符串值,而且這個字符串值的長度小於39字節,那麼字符串對象將使用embstr編碼的方式來保存這個字符串值。
embstr編碼方式是專門用於保存短字符串的一種優化編碼方式,這種編碼和raw編碼同樣,都是由redisObject結構和sdshdr結構來表示字符串對象,但raw編碼會調用兩次內存分配函數來分別建立redisObject結構和sdshdr結構,而embstr編碼則經過調用一次內存分配函數來分配一塊連續空間,空間中依次包含redisObject和sdshdr兩個結構,
embstr編碼的字符串在執行命令時,產生效果和raw編碼的字符串對對象執行命令時產生的效果是相同的,但使用embstr編碼的字符串對象保存短字符串值有如下好處:
1. embstr編碼將建立字符串對象所需的內存分配次數從raw編碼的兩次下降爲一次。
2. 釋放embstr編碼的字符串對象只須要一次內存釋放函數,而釋放raw編碼的字符串對象須要調用兩次內存釋放函數。
3. 由於embstr編碼的字符串對象的全部數據都保存在一塊連續的內存裏面,因此這種編碼的字符串對象比其raw編碼的字符串對象可以更好地利用緩存帶來的優點。
最後要說的是,能夠用long double類型標識的浮點數在Redis中也是做爲字符串值來保存的。若是咱們要保存一個浮點數到字符串對象裏面,那麼程序會先將這個福電視轉換成字符串值,而後再保存轉換所得的字符串值。在須要的時候,程序會將保存在字符串對象裏面的字符串值轉換回福電視值,執行某些操做,而後再執行操做所得的浮點數值轉換回字符串值,並繼續保存在字符串對象裏面。
字符串對象保存各種型值的編碼方式:
值 | 編碼 |
能夠用long類型保存的整數 | int |
能夠用long double類型保存的浮點數 | embstr或raw |
字符串值,或者由於長度太大而沒辦法用long類型表示的整數,又或者由於長度太大而沒辦法用long double類型表示的浮點數 | embstr或者raw |
int編碼的字符串對象和embstr編碼的字符串對象在條件知足的狀況下,會被轉化爲raw編碼的字符串對象。
對於int編碼的字符串對象來講,若是咱們向對象執行了一些命令,使得這個對象保存的再也不是整數值,而是一個字符串值,那麼字符串對象的編碼將從int變爲raw。
另外,由於Redis沒有爲embstr編碼的字符串對象編寫任何相應的修改程序(只有int編碼的字符串對象和embtsr編碼的字符串對象有這些程序),因此,embstr編碼的字符串對象實際上只是只讀的。當咱們對embstr編碼的字符串對象執行修改命令時,程序會先將對象的編碼從embstr轉換成raw,而後再執行修改命令。由於這個緣由,embstr編碼的字符串對象在執行修改命令以後,總會變爲一個raw編碼的字符串對象。
列表對象:
列表對象的編碼能夠是ziplist或者是linkedlist.
1. ziplist編碼的列表對象使用壓縮列表做爲底層實現,,每一個壓縮列表節點(entry)保存了一個列表元素。
2. linkedlist編碼的列表對象使用雙端鏈表做爲底層實現,每一個雙端鏈表節點(node)都保存了一個字符串對象,而每一個字符串對象都保存了一個列表元素。
注意:linkedlist編碼的列表對象在底層的雙端鏈表結果中包含了多個字符串對象,這種嵌套字符串對象的行爲在稍後介紹的哈希對象、集合對象和有序集合對象中都會出現,字符串對象是Redis五種類型的對象中惟一一種會被其餘四種對象嵌套的對象。
編碼轉換:
當列表對象知足如下兩個條件時,列表對象使用ziplist編碼:
1. 列表對象保存的全部字符串元素的長度都小於64字節;
2. 列表對象保存的元素數量小於512個;不能知足這兩個條件的列表對象使用linkedlist編碼。
對於使用ziplist編碼的列表對象來講,當使用ziplist編碼所需的兩個條件中任意一個不能被知足時,對象的編碼轉換操做就會被執行。本來保存在壓縮列表裏的全部元素都會被轉移並保存在雙端鏈表裏面,對象的編碼也會被變爲linkedlst。
哈希對象:
哈希對象的編碼能夠爲ziplist和hashtable;
一、ziplist編碼的哈希對象使用壓縮列表做爲底層實現,每當有新的鍵值對須要加入哈希對象時,程序會先將保存了鍵的壓縮列表節點推入到壓縮列表節點末尾,而後再講保存了值的壓縮列表節點推入壓縮列表節點末尾。因此保存了同一鍵值對的兩個節點老是緊挨在一塊兒,保存鍵的節點在前面,保存值得節點在後面;先添加到哈希對象的鍵值對會被放在壓縮列表的表頭方向,然後來的壓縮列表節點會被放在壓縮列表的表尾方向。
舉個例子:執行HSET命令,服務器將會建立一個列表對象做爲profile的值:
redis>HSET profile name "Tom" (integer) 1 redis>HSET profile age 25 (integer) 1 redis>HSET profile career "Programmer" (integer) 1
若是profile鍵的值對象使用的是ziplist編碼,那麼值對象將會是下圖所示,這個壓縮列表對象使用的底層實現以下下圖所示:
2. hashtable編碼的哈希對象使用字典做爲底層實現,哈希對象中的每一個鍵值對都使用一個字典鍵值對來保存:
a. 字典的每一個鍵都是一個字符串對象,對象中保存了鍵值對的鍵;
b. 字典的每一個值都是一個字符串對象,對象中保存了鍵值對的值;
舉個例子:若是前面profile鍵建立的不是ziplist編碼的哈希對象,而是hashtable編碼的哈希對象,則該哈希對象如圖所示:
編碼轉換:
當哈希對象能夠同時知足一下兩個條件時,哈希對象使用ziplist編碼:
1. 哈希對象保存的鍵值對的鍵和值的字符串換長度都小於64字節;
2. 哈希對象保存的鍵值對數量小於512個;
不能知足這兩個條件的哈希對象須要使用hashtable編碼;
對於使用zuolist編碼的列表對象來講,當使用ziplist編碼所知足的兩個條件中的任意一個不能被知足時,對象的編碼轉換操做就會被執行,本來保存在壓縮列表裏的全部鍵值對就會被轉移並保存到字典裏面,對象的編碼也會從ziplist變未hashtable。
備註:除了鍵的長度太大會引發編碼轉換之外,值得長度太大也會引發編碼轉換。
集合對象:
集合對象的編碼能夠是intset或者是hashtable;
1. intset編碼的集合對象使用整數集合做爲底層實現,集合對象包含的全部的元素都保存在整數結婚裏面。
舉個例子:如下代碼將建立一個下圖所示的intset編碼的及覈對象:
redis> SADD numbers 1 3 5 (integer) 3
2. hashtable編碼的集合對象使用字典做爲底層實現,字典的每個鍵都是一個字符串對象,每一個字符串對象包含了一個集合元素,而字典的值則所有被設置爲NULL。
舉個例子:如下代碼將建立一個以下圖所示的hashtable編碼集合對象:
編碼的轉換:
當集合對象能夠同時知足如下兩個條件時,對象使用intset編碼:
1. 集合對象保存的全部元素都是整數值;
2. 集合對象保存的元素數量不超過512個;
不能滿這兩個條件的集合對象須要使用hashtable編碼。
有序集合對象:
有序集合的編碼能夠是ziplist或者是skiplist.
1. ziplist編碼的有序集合對象使用壓縮列表做爲底層實現,每一個集合元素使用兩個緊挨在一塊兒的壓縮列表節點來保存,第一個節點保存元素的成員(member),而第二個節點保存元素的分值(score)。
壓縮列表的集合元素按照分值大小從小到大排序,分值較小的元素被放在靠近表頭的位置,分值較大的元素被放在靠近表尾的位置。
舉個例子:執行ZADD命令,那麼服務器將會建立一個有序集合對象做爲price鍵的值:
redis>ZADD price 8.5 apple 5.0 banana 6.0 cherry (integer) 3
若是price鍵的值對象使用的是ziplist編碼,那麼這個值對象將會是下圖所示:
2. skiplist編碼的有序集合對象使用zse結構做爲底層實現,一個zset結構同時包含一個字典和一個跳躍表:
typedef struct zset{ zskiplist *zsl; dict *dict; }zset;
zset 結構中的zsl跳躍表按分值從小到大保存了全部集合元素,每一個跳躍表保存了一個集合元素:跳躍表的object屬性保存了元素的成員,而跳躍表節點的score屬性則保存了元素的分值。經過這個跳躍表,程序能夠對有序集合進行範圍型操做。
除此以外,zset結構中的dict字典爲有序集合建立了一個成員到分支的映射,字典中的每個鍵值對都保存類一個集合元素:字典的鍵保存了元素的成員,而字典的值則保存了元素的分值。經過這個字典,程序能夠經過O(1)複雜度查找給定成員的分值,ZSCRE命令就是根據這一特性實現的。值得一提的是,雖然zset結構同時使用跳躍表和字典保存有序集合元素,可是這兩種數據結構都會經過指針來共享相同元素的成員和分值,因此同時使用跳躍表和字典來保存集合元素不會產生任何重複成員或者分值,也不會所以而浪費額外的內存。
爲何有序集合要同時使用跳躍表和字典來實現?
同時使用確定是由於性能有所提升!
緣由:
1. 若是隻是使用了字典來保存有序集合,那麼雖然是以O(1)的時間複雜度來查找成員的分值這一特性會被保留,可是,由於字典是以無序的方式來保存集合元素,因此每次在執行範圍型操做——好比:ZRANK、ZRANGE等命令時,程序都要對字典保存的全部元素進行排序,完成這種排序操做須要至少O(NlogN)時間複雜度,以及額外的O(N)內存空間(由於要建立一個數組來保存排序後的元素)
2. 若是隻是使用了跳躍表來實現有序集合,那麼跳躍表執行範圍型操做的全部優勢都會被保留,但由於沒有了字典,因此根據成員查詢號分值這一操做的複雜度就會從O(1)上升到O(logN)。
舉個例子:若是前面price鍵建立的不是ziplist比那麼的有序集合,而是skiplist編碼的有序結合對象。那麼這哥有序集合對象將會是下圖所示:而對象所使用的zset結構將會是下下圖所示:
編碼的轉換:
當有序集合對象能夠同時知足如下兩個條件時,對象使用ziplist編碼:
1.有序集合保存的元素數量小於128個;
2.有序集合保存的全部元素的長度都小於64個字節;
不能知足以上兩個條件的有序集合對象都使用skiplist編碼;
類型檢查和命令多態:
Redis中用於操做鍵的命令能夠分爲兩種類型:
1. 能夠對任何類型鍵執行的命令:好比DEL命令、EXPIRE命令、RENAME命令、TYPE命令、OBJECT命令等。
2. 而另外一種命令只能對特定類型的鍵執行。好比:
SET、GET、APPEND、STRLEN等命令只能對字符串鍵執行;
RPUSH、LPOP、LINSET、LLEN等命令只能針對列表鍵執行;
SADD、SPOP、SINTER、SCARD等命令只能針對集合鍵執行;
ZADD、ZCARD、ZRANK、ZSCORE等命令只能針對有序集合鍵執行;
若是針對某類型鍵執行其它特定類型的鍵的命令,Redis就會返回一個類型錯誤。
類型檢查的實現:
在執行一個類型特定鍵的命令以前,Redis會檢查輸入鍵的類型是否正確,而後再決定是否執行給定的命令。
類型特定命令所進行的類型檢查是經過redisObject結構的type屬性來實現的:
1. 在執行一個類型特定命令以前,服務器會先檢查輸入數據庫鍵的值對象是否爲執行命令所需的類型,若是是的話,服務器就會對鍵執行指定的命令。
不然,服務器將拒絕執行命令,並想客戶端返回一個類型錯誤。
下圖爲類型檢查過程(以LLEN命令爲例):
多態命令的實現:
Redis除了根據值對象的類型來判斷鍵是否可以執行指定命令以外,還會根據值對象的編碼方式,選擇正確的命令實現代碼執行命令。
以列表對象爲例:
1. 若是列表對象的編碼爲ziplist,則說明列表對象的編碼爲壓縮列表,程序將會使用ziplistLen函數來返回列表的長度。
2.若是列表對象的編碼爲linkedlist,則說明列表對象的編碼爲雙端鏈表,程序將會使用linkedlist函數來返回列表長度。
借用面向對象的術語回答,能夠認爲LLEN命令時墮胎的,執行執行LLEN命令時列表鍵,那麼不管值對象使用的是ziplist編碼或者是linkedlist編碼,命令均可以正常執行。
內存回收:
由於C語言不具有內存回收功能,因此Redis在本身對象系統中構建了一個引用計數(reference counting)技術實現的內存回收機制,經過這一機制,程序能夠經過跟蹤對象的引用計數信息,在適當的是否自動釋放對象並進行內存回收。
每一個引用計數信息都是由redisObject結構的refcount屬性記錄:
typedef struct redisObject{ //... //引用計數 int refcount; //... }robj;
對象的引用計數信息會隨着對象的使用狀態而不斷變化:
1. 在建立一個新對象時,引用計數信息的值會被初始化爲1;
2. 在對象唄一個新程序使用時,它的引用計數值會被增一;
3. 在對象再也不被一個程序使用時,它的引用計數值會被鍵一;
4. 當對象的引用計數值變爲0 時,對象所佔用內存會被釋放;
下表即爲修改對象引用計數屬性的API:
函數 | 做用 |
incrRefCount | 將對象的引用計數增一 |
decrRefCount | 將對象的引用計數減一,當對象的引用計數值爲0時,釋放對象 |
resetRefCount | 將對象的引用計數值爲設置爲0,但並不釋放對象,這個函數一般在須要從新設置對象的引用計數值時使用。 |
對象的整個生命週期能夠劃分爲建立對象、操做對象、釋放對象三個階段。做爲例子,如下代碼展現了一個字符串對象從建立到被釋放的過程:
//建立一個字符串對s,對象的引用計數值爲1 robj *s = createStringObject(...) //對象s執行各類操做.... //將對象s的引用計數減一,是的對象的引用計數變爲0 //致使對象s被釋放 decrRefCount(s)
對象共享:
除了用於實現引用計數內存回收機制以外,對象的引用計數屬性還帶有對象共享的做用。
在Redis中,讓多個鍵共享同一個值對象須要執行兩個步驟:
1. 將數據庫鍵的值指針指向一個現有的值對象。
2. 將被共享的值的引用計數增一。
共享對象機制對於節約內存很是具備幫助,數據庫中保存的相同值對象越多,對象共享機制就能節約越多的內存。
目前來講,Redis在初始化服務器時,建立一萬個字符串對象,這些對象包含了從0到9999的全部整數值,當服務器須要用到值0到9999的字符串對象時,服務器就會使用這些共享對象,而不是新建立對象、
另外,這些共享對象不只只有字符串鍵可使用,那些在數據結構中嵌套了字符串對象的對象(linkedlist編碼的列表對象、hashtable編碼的哈希對象、hashtable編碼的集合對象、以及zset編碼的有序集合對象)均可以使用這些共享對象。
爲何Redis不共享包含字符串的對象?
當服務器考慮將一個共享對象設置爲鍵的值對象時,程序須要先檢查給定的共享對象和鍵想要建立的目標對象是否相同,只有在共享對象和目標對象徹底相同的狀況下,程序纔會將共享對象做爲鍵的值對象,而一個共享對象的保存的值越複雜,驗證共享對象和慕白對象的時間複雜度就越高。消耗的CPU就越多。
1. 若是共享對象是保存整數值的字符串對象,那麼驗證操做的複雜度爲O(1)。
2. 若是共享對象是包含字符串值的字符串對象,那麼驗證操做的複雜度爲O(N)。
3. 若是共享對象是包含了多個值(或者對象)的對象,好比列表對象或者哈希對象,那麼驗證操做的複雜度爲O(N^2)。
所以,儘管共享更復雜的對象能夠節約更多的內存,但受到CPU時間的限制,Redis只對包含整數值的字符串對進行共享。
空轉時長:
除了前面介紹的tyoe、encoding、ptr和refCount屬性以外,redisObject還有最後一個屬性爲lru屬性。該屬性記錄了對象最後一次被命令程序訪問的時間。
typedef struct redisObject{ //... unsigned lru:22; //... }robj;
1. OBJECT IDLETIME命令能夠打印出給定鍵的空轉時長,就是經過將當前時間減去值對象的lru屬性計算得出。
2. 鍵的空轉時長還有另外一項做用:若是服務器打開了maxmemory選項,而且服務器回收內存的算法爲volatile-lru或者alkeys-lru,那麼當服務器佔用的內存數超過了maxmemory選項設置的上限值時,空轉時長較高的那部分鍵會優先被服務器是否,從而回收內存。