第一部分 資料結構與物件
簡單動態字串(SDS simple dynamic string)
-
Redis沒有直接使用C語言傳統的字串表示,而是自己構建了SDS這樣的抽象資料型別,並
將SDS作為Redis預設字串
表示 -
SDS示例
-
SDS與C字串的區別
-
常數複雜度獲取字串長度
- C字串需要遍歷字元陣列並計數,時間複雜度為O(N)
- 而SDS的len屬性記錄了已佔用長度,時間複雜度為O(1)
-
杜絕緩衝區溢位
- 拼接字串的過程中 ①申請空間 ②擴充字串 ;對C字串而言,如果忘記了手動申請空間則會造成空間溢位 . 對sds提供的api而言,會自動進行判斷待擴充字串長度是否大於 free屬性. 如果大於就自動進行空間申請,因此不會造成空間溢位
-
減少修改字串時帶來的記憶體重分配次數
sds通過未使用空間解除了字串長度與底層陣列長度之間的關聯 (charLen+1= bufLen)
-
空間預分配
-
惰性空間釋放
關於空間預分配和惰性空間釋放
- 字串增長操作時,如果需要修改長度小於1M則分配該字串長度2倍的記憶體空間,如果修改後長度大於等於1M則分配該字串長度+1的記憶體空間(預分配,避免每次增長操作都需要進行記憶體重分配執行系統呼叫)
- 字串縮短操作時,程式不會立即釋放縮短後多出來的位元組,而是使用free屬性將這些位元組的數量記錄下來,並等待將來使用(惰性釋放,避免以後需要增長操作時分配記憶體. 並且提供真正釋放空間的API,避免造成記憶體浪費)
-
-
二進位制安全的,不是以空字元
'\0'
來判斷字串是否結束,而是以 屬性len -
遵循C字串以空字元結尾的管理,可以相容部分C字串函式
-
連結串列
-
雙向無環連結串列
-
記錄表頭和表尾結點,獲取頭和尾結點時間複雜度為O(1)
-
記錄連結串列長度,獲取連結串列時間複雜度為O(1)
-
多型 , value能儲存任意型別資料
字典
- 字典被廣泛用於實現Redis的各種功能,其中包括資料庫和雜湊鍵
- 使用鏈地址法解決衝突,當多個鍵倍分配到相同雜湊索引時將新鍵新增到節點連結串列表頭
- 字典包含ht[0]和ht[1] (ht[1]僅為rehash時使用)兩個雜湊表,當雜湊表儲存的鍵值對數量太多或太少時使用重新雜湊rehash 維持雜湊表的負載因子在合理的範圍內
- Redis使用MurmurHash2演算法(讓雜湊值儘量隨機分散)來計算鍵的雜湊值
- 考慮到資料量太大時,rehash過程可能會耗費很多時間而停止服務,因此使用的是漸進式rehash . 將rehash鍵值對所需的計算工作均攤到對 字典的每個新增,刪除,查詢,修改操作中, 避免了集中式rehash帶來的龐大計算量
漸進式rehash步驟
1. 為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個雜湊表
2. 在字典中維持一個索引計數器rehashidx,並將值設為0,表示rehash工作開始
3. 在rehash期間, 每次對字典執行[新增,刪除,查詢或者更新]操作時,程式除了執行的操作外,還會順帶將ht[0]雜湊表在rehashIdx索引上的鍵值對rehash到ht[1]中,當本次rehash結束後,rehashidx屬性值+1
4. 隨著字典操作的不斷執行,在某個時間點上,ht[1]所有鍵值對都會被rehash到ht[1],此時程式將rehashidx的屬性值設為-1,表示操作已完成.
複製程式碼
跳躍表
跳躍表是一種有序資料結構,它通過在每個節點中維持多個指向其他節點的指標,從而達到快速訪問節點的目的
跳躍表的相關性質介紹: 我就是那個地址
場景
對於一組有序的集合 如果用陣列來表示: 查詢可以使用二分法,時間複雜度為O(logn),但插入和刪除需要O(n) 如果用連結串列表示: 查詢為O(n), 增加刪除為O(1) (已知待更改位置的前驅節點). 使用跳躍表來加速連結串列的查詢速度 : 每個節點維持多個指向其他節點的指標
跳躍表的性質:
- 跳躍表的每一層都是一條有序的連結串列
- 跳躍表的查詢次數接近層數, 查詢的時間複雜度為O(logn),插入,刪除也為O(logn).
- 最底層的連結串列包含所有元素
- 跳躍表是一種隨機化的資料結構( 插入時通過拋硬幣決定跨越層數 )
- 跳躍表的空間複雜度為O(n)
下圖展示了一次查詢9的過程
① 在第4層 , 0<9<10 因此跳到第三層,查詢(0,10)區間
②在第3層 , 6<9 因此跳到第二層,查詢 (6,10)區間
③在第2層 , 8<9 因此跳到第一層,查詢(8,10)區間
④在第1層, 9=9 找到目標數字
跳躍表的實現
Redis中的跳躍表
- 跳躍表是有序集合的底層實現之一
- Redis的跳躍表實現由zskiplist 和 zskiplistNode兩個結構組成,其中zskiplist用於儲存跳躍表資訊(比如表頭節點 , 表尾節點 , 長度) , 而zskiplistNode用於表示跳躍表節點.
- 每個跳躍表節點的層高都是1至32之間隨機數
- 在同一個跳躍表中,多個節點可以包含相同的分數,但每個節點的成員物件必須是唯一的.
- 當分值相同時,節點按成員物件的字典排序大小進行排序
整數集合
整數集合是集合鍵的底層實現之一,當一個集合
只包含整數值元素,並且這個集合的元素數量不多時
,Redis就會使用整數集合作為集合鍵的底層實現
目的: 儘可能的節約記憶體
-
可以儲存 int16_t , int32_t , int64_t 三種型別的整數集
-
整數集合的底層實現為陣列,這個陣列以有序,無重複的方式儲存集合元素
-
為了節約記憶體, 集合型別使用最小型別儲存整數, 僅當新新增的整數大於當前所能容納的值的範圍時進行升級操作
-
新增,刪除元素時間複雜度為O(n) , 查詢的時間複雜度為log(n)
-
不支援降級操作
升級步驟 1. 根據新元素的型別擴充套件底層陣列空間,併為新元素分配空間 2. 轉換現有元素至新的型別,保持有序性防止元素 3. 新增元素,當新元素小於現有元素時放在索引0, 當新元素大於所有現有元素時放在索引length-1的位置 複製程式碼
壓縮列表
-
壓縮列表(ziplist)是列表鍵和雜湊鍵的底層實現之一. 當一個列表鍵只包含少量列表項,並且每個列表項要麼是小整數值,要麼是長度比較短的字串時,那麼Redis會使用壓縮列表來做列表項的底層實現
-
列表項是Redis為了節約記憶體而開發的,是由一系列特殊編碼的連續記憶體塊組成的順序性資料結構.
壓縮列表的構成
壓縮列表:
zlbytes: 列表總長 zltail:距離表尾偏移量 zllen:節點個數 zlend:特殊值,用於標記壓縮列表末尾
壓縮列表節點:
previous_entry_length:前一項節點的長度(用於從尾向頭遍歷)
encoding:記錄節點content的型別和長度(位元組or字元) contents:節點內容
複製程式碼
連鎖更新現象
previous_entry_length用於儲存前一項節點的長度
當前一項長度小於254位元組 該屬性用1位元組表示; 當前一項大於254位元組,該屬性用5位元組表示.
-
插入時連鎖更新
當e1為250-253個位元組 , 並且previous_entry_length為1個位元組時, 插入New大於254位元組,因此e1.previous_entry_length需要變為5位元組,可能會連鎖e2 e3的previous_entry_length屬性也跟著變大
-
刪除時連鎖更新
當e1為250-253個位元組 , 並且previous_entry_length為1個位元組時, 刪除small,因此e1的前一個節點變為較大的big節點. 導致e1.previous_entry_length需要變為5位元組,可能會連鎖e2 e3的previous_entry_length屬性也跟著變大
總結
- 壓縮列表是一種為節約記憶體而開發的順序型資料結構
- 壓縮列表被用作列表鍵和雜湊鍵的底層實現之一
- 壓縮列表可以包含多個節點,每個節點可以儲存一個位元組陣列或整數值
- 新增或刪除節點時,可能會引發連鎖更新操作, 時間複雜度為O(N^2),但機率不高
物件
物件的結構
五種物件型別
型別常量 | 物件名稱 |
---|---|
REDIS_STRING | 字串物件 |
REDIS_LIST | 列表物件 |
REDIST_HASH | 雜湊物件 |
REDIS_SET | 集合物件 |
REDIS_ZSET | 有序集合物件 |
底層資料結構
編碼常量 | 對應的底層資料結構 |
---|---|
REDIS_ENCODING_INT | long型別的整數 |
REDIS_ENCODING_EMBSTR | embstr編碼的簡單動態字串 |
REDIS_ENCODING_RAW | SDS |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 雙向無環連結串列 |
REDIS_ENCODING_ZIPLIST | 壓縮列表 |
REDIS_ENCODING_INTSET | 整數集合 |
REDIS_ENCODING_SKIPLIST | 跳躍表 |
物件的編碼轉換
主要從記憶體分配和效率方面進行考慮.
當資料量較小時, 可以使用連續分配+壓縮的方式, 提高記憶體利用率與效率
當資料量較大時, 可以使用離散分配方式 , 能更好的利用空閒的分割槽
字串物件 String
字串物件的編碼可以是int , raw(sds) 或者embstr** (redisObject和底層資料結構記憶體連續的sds)
值 | 編碼 |
---|---|
可以用long型別儲存的整數 | int |
可以用long double表示的浮點數 | raw或embstr |
字串值,或者因為長度而無法用long型別儲存的整數 ,或者因為長度而無法用long double表示的浮點數 |
raw或embstr |
列表物件 List
列表物件的編碼可以是ziplist或者linkedList
值 | 編碼 |
---|---|
列表物件儲存的所有字串長度小於64位元組 列表物件元素數量小於512個 |
ziplist |
不滿足上訴二者條件之一 | linkedList |
雜湊物件 Hash
列表物件的編碼可以是ziplist或者hashtable
值 | 編碼 |
---|---|
所有鍵值對的鍵和值的字串長度都小於64位元組 鍵值對數量小於512個 |
ziplist |
不滿足上訴二者條件之一 | hashtable |
集合物件 Set
集合物件的編碼可以是intset或者hashtable(hashtable的值為NULL)
值 | 編碼 |
---|---|
集合物件儲存的元素都是整數值 鍵值對數量小於512個 |
intset |
不滿足上訴二者條件之一 | hashtable |
有序集合物件 Sorted Set
有序集合物件的編碼可以是ziplist或者skiplist(hashtable的值為NULL)
值 | 編碼 |
---|---|
有序集合儲存的元素數量小於128個 有序集合儲存的所有元素成員的長度都小於64位元組 |
ziplist |
不滿足上訴二者條件之一 | skiplist |
總結
- Redis資料庫中每個鍵值對的鍵和值都是一個物件
- Redis共有字串, 列表 , 雜湊, 集合 , 有序集合五種型別的物件,每種型別的物件至少由兩種以上的編碼方式,不同的編碼可以在不同場景上優化物件使用效率
- 伺服器在執行某些命令之前,會先檢查給定鍵的型別能否執行指定的命令, 即檢查鍵的值物件的型別
- Redis的物件系統帶有引用計數實現的記憶體回收機制,當一個物件不再被使用時,該物件所佔用的記憶體會被自動釋放
- Redis會共享值為0到9999的字串物件
- 物件會記錄自己的最後一次被訪問的時間,這個時間可以用於計算物件的空轉時間(LRU)