跳躍表:
跳躍表是一種有序數據結構,通過在每個節點維持多個指向其他節點的指針,達到快速訪問節點的目的。redis使用跳躍表作為有序集合鍵的實現,如果一個有序集合包含額元素數量比較多,又或者有序集合中元素的成員是比較長的字符串時,redis會使用
跳躍表作為有序集合鍵的實現。
redis只在有序集合鍵和集群節點中用作內部數據結構。
跳躍表zskiplist{
zskiplistnode header :指向跳躍表的表頭節點。
zskiplistnode tail:指向跳躍表的表尾節點
level:記錄目前跳躍表,層數最大的節點所在層數
length:記錄跳躍表長度,不包含表頭節點
}
zskiplistnode{
zskiplistnode *backward 后退指針,在程序從表尾向前遍歷時使用
score 分數
*obj 成員對象
zslistlevel{
zskiplistnode *forward 訪問位於表尾方向的其他節點
int span 表示當前節點與本層所指向的下一個節點的距離
}
}
節點的分值是一個double類型的浮點數,跳躍表中的所有節點都按分值大小排序
節點的成員對象是一個指針,它指向一個字符串對象,而字符串對象則保存着一個sds值。
同一個跳躍表,各個節點保存的成員對象必須是唯一的,但是多個節點保存的分值可以是相同的:分值相同的節點將按照成員對象在字典序中的大小來進行排序,成員對象較小的節點會排在前面,靠近表頭方向,而成員對象
較大的節點則會排在后面。靠近表尾方向。
整數集合:
整數結合是集合鍵的底層實現,
intset{
encoding 編碼方式:int16_t int32_t int64_t
int8_t contents[] 保存元素的數組 //contents數組所保存的值取決於編碼的方式,且數組中的元素都是從小到大排序的
}
整數集合的升級操作,如果向數組中元素添加了編碼方式更高的整數,則會對整數集合進行升級
根據新元素的類型,擴展底層數組空間的大小,並為新元素分配空間,將底層數組現有的元素進行類型轉換,轉換到與新元素相同的類型,並將轉換后的元素放在正確的位置上,
這個正確的位置,要維持數組中元素的大小排序,最后一步需要改變encoding的值為最高類型的值
整數集合的優點是可以通過自動升級底層數組來適應新元素,可以隨意將不同類型編碼的整數,加入到集合中。同時
可以節約內存,只有在添加更高編碼的元素時才會升級集合,可以盡量節省內存。
整數集合不支持降級操作。
壓縮列表:
壓縮列表是哈希鍵與列表鍵的底層實現,當一個列表鍵只包含少量列表項,並且列表項要么就是小整數值,長度比較短的字符串,那么reids會使用壓縮列表來做列表鍵的底層實現。
壓縮列表是redis為了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序性數據結構,一個壓縮列表可以包含任意多個節點,每個節點保存一個字節數組或者整數值
壓縮列表中的壓縮節點即entryx的構成:
{
previous_entry_length
encoding
content
}entryx
previous_entry_length 保存的是前一個字節的長度,如果前一個節點的長度<254字節,那么previous_entry_length屬性的長度為一字節(注:前一個節點的長度>=254字節,指實際分配內存空間大於254字節,而我們這里所說的屬性長度為一字節僅僅是用來記錄這個長度值,相當於記錄數字,這里容易弄混),如果前一個節點長度>=254字節,那么previous_entry_length屬性的長度為5字節,其中屬性的第一字節會被設置為0xEE(十進制254),而之后的四個字節則用於保存前一字節的長度
壓縮列表可以通過表尾指針所指向表尾的節點減去當前及節點的previous_entry_length值,即可以得到前一個節點所在位置,這就是壓縮列表從表尾向表頭的遍歷操作
壓縮列表存在連鎖更新問題:即如果在表頭插入一個大於254字節長度的節點,原來表頭節點的previous_entry_length屬性僅為一字節無法保存現表頭長度,則需要變化擴展previous_entry_length屬性為5字節,如果恰好當前節點長度介於250到253之間,則擴展后使得當前節點長度>=254字節,使得下一個節點也無法保存,繼續擴展下一個節點可能出現連鎖反應,引發由內存空間重分配引發的性能問題。
redis對象:
redis沒有使用之前講到的數據結構來實現鍵值對數據庫,而是基於這些數據結構創建了一個對象系統,系統包含了 字符串對象,列表對象,哈希對象,集合對象,有序集合對象,五種類型
reredis使用對象來表示數據庫中的鍵和值,每次使用reids的數據庫創建一個鍵值對時,至少會創建兩個對象,
redisobject結構如下所示:
redisobject{
unsigned type :上面提到的五種類型
encoding:編碼:對向的ptr指針指向對象的底層實現數據結構,而這些數據結構由對象的encoding屬性決定
void * ptr 指向底層實現數據結構的指針
}
記住每一個種對象所使用的的編碼方式中種類可以有多種,例如 String 對象編碼方式可以有 整數值或者采用embstr編碼的簡單動態字符串實現
即 redis_striing 編碼方式有 redis_encoding_int 和 redis_encoding_embstr
使用object encoding 命令可以查看一個數據庫鍵的值對象的編碼
字符串對象的編碼可以是int raw embstr
當字符串對象所保存字符串的值小於等於39字節,那么將采用embstr編碼
embstr編碼是專門用於保存段字符串的一種優化編碼方式,這種編碼和raw編碼一樣都是用redisobject結構和sdshdr結構,而embstr編碼則通過調用一次內存分配函數來分配一塊連續的空間,
空間中依次包含redisobject和sdshdr兩個結構。embstr編碼優點將次分配內存和釋放內存對於系統調用的次數,同時enmstr編碼的字符串對象所有的數據都保存在一塊連續的內存里面,所以采用embstr編碼的字符串對象比raw編碼的字符串對象
能夠更好地利用緩存帶來的優勢。
列表對象可以采用壓縮列表與雙端鏈表編碼:
當列表對象可以滿足下列兩個條件時,列表對象可以使用ziplist編碼:
列表對象所保存的所有字符串元素長度都小於64字節,
列表對象保存的元素數量小於512個,
如果不能滿足這兩個條件,則會采用linkedlist編碼。
哈希對象的編碼可以是ziplist和hashtable
對於壓縮列表:保存了同一個鍵值對的兩個節點總是緊挨在一起,保存鍵的節點在前,保存值得節點在后;先添加到哈希對象中的鍵值對會被放到壓縮列表的表頭方向,而后來添加到哈希表對象中的鍵值對會被放到
壓縮列表的表尾方向。
hashtable編碼的哈希對象底層使用字典作為頂層實現。哈希對象中的每個鍵值對使用一個字典鍵值對來保存
編碼轉換:
當哈希對象保存的所有鍵值對的鍵和值的字符串長度都小於64字節,
哈希對象保存的鍵值對數量小於512個,不能滿足這兩個條件的哈希對象需要使用hashtable編碼
集合對象的編碼可以是intset或者hashtablle
如果是使用hashtable編碼作為集合的底層實現,字典的每個鍵都是一個字符串對象,每個字符串對象包含了一個集合元素,而字典的值則全部被設置為null。
有序集合對象:
編碼可以是ziplist和skiplist
壓縮列表作為頂層實現,則壓縮列表內的集合元素按照分值從小到大進行排序,分值較小的元素被放置在靠近表頭的位置,而分值較大的元素則被放在靠近表尾的位置
較復雜的是skiplist編碼的有序集合對象使用zset結構作為底層實現,一個zset結構同時包含一個字典和一個跳躍表
zet{
zskiplist *zsl;
dict *dict
}zset
對於zskiplist使用跳躍表節點的score屬性保存分值,object屬性保存元素成員
對於字典使用,字典的鍵保存了元素的值,字典的值保存了元素的得分
這樣的目的是使用zskiplist可以降低范圍查詢的時間復雜度:如執行zrank,zrange命令
使用dict的原因是可以以O(1)的時間復雜度的到元素的分值,如果上述兩只結構只使用一個的話,必然會導致某一個命令的時間復雜度上升。
有序集合對象可以同時滿足一下兩個條件時對象會使用ziplist編碼:
有序集合對象保存元素數量小於128
有序集合保存的所有元素成員的長度都小於64字節
redisobject對象中內存回收:
redis在自己的對象系統中構建了一個引用計數技術實現內存的回收機制,通過這一機制,程序通過跟蹤對象的引用計數信息,在適當的時候自動釋放對象進行內存回收
每個對象的引用計數用redisobject對象的refcount 屬性記錄
創建一個新對象時,引用計數值會被初始化為1
當對象被一個新程序使用時,它的引用計數值會被增1
當對象不再被一個程序使用時,它的引用計數值減一
當對象的引用計數值為0時,對象占用的內存會被釋放
redisobject對象中對象共享:
對象的引用計數屬性還帶有對象共享的作用,如果鍵a創建了一個包含整數值100的字符串對象作為值對象,如果鍵b也要創建一個同樣保存了整數值100的字符串對象作為值對象,
那么鍵b則會和a共享一個字符串對象
實現步驟:
將數據庫鍵的值指針指向一個現有的值對象,將被共享的值對象的引用計數增1
目前來說redis會在初始化服務器時,創建一萬個字符串對象,這些對象包含了從1到9999的所有整數值,當服務器需要用到0到9999的字符串對象時,服務器就會使用這些共享對象,而不是創建新對象
對象的空轉時長:
redisobject.lru屬性:該屬性記錄了對象最后一次被命令程序訪問的時間。
通過使用object idletime 命令可以打印出給定鍵的空轉時長,這一空轉時長就是通過將當前時間減去鍵的值對象的lru時間計算得到
鍵的空轉時長還有一個作用就是如果服務器打開了maxmemory選項,並且服務器用於內存回收的算法為volatile-lru或者allkeys-lru,那么當服務器占用的內存數超過了maxmemory選項所設置的上限值時,
空轉時長較高的那部分鍵會優先被服務器釋放,從而回收內存