簡讀筆記_Redis設計與實現_第一章_資料結構與物件

你的頭髮真的好長發表於2019-04-21

第一部分 資料結構與物件

簡單動態字串(SDS simple dynamic string)

  • Redis沒有直接使用C語言傳統的字串表示,而是自己構建了SDS這樣的抽象資料型別,並將SDS作為Redis預設字串表示

  • SDS示例

    簡讀筆記_Redis設計與實現_第一章_資料結構與物件

  • 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字串函式

連結串列

簡讀筆記_Redis設計與實現_第一章_資料結構與物件

  • 雙向無環連結串列

  • 記錄表頭和表尾結點,獲取頭和尾結點時間複雜度為O(1)

  • 記錄連結串列長度,獲取連結串列時間複雜度為O(1)

  • 多型 , value能儲存任意型別資料

字典

簡讀筆記_Redis設計與實現_第一章_資料結構與物件

  • 字典被廣泛用於實現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) (已知待更改位置的前驅節點). 使用跳躍表加速連結串列的查詢速度 : 每個節點維持多個指向其他節點的指標

跳躍表的性質:

  1. 跳躍表的每一層都是一條有序的連結串列
  2. 跳躍表的查詢次數接近層數, 查詢的時間複雜度為O(logn),插入,刪除也為O(logn).
  3. 最底層的連結串列包含所有元素
  4. 跳躍表是一種隨機化的資料結構( 插入時通過拋硬幣決定跨越層數 )
  5. 跳躍表的空間複雜度為O(n)

下圖展示了一次查詢9的過程

簡讀筆記_Redis設計與實現_第一章_資料結構與物件

① 在第4層 , 0<9<10 因此跳到第三層,查詢(0,10)區間

②在第3層 , 6<9 因此跳到第二層,查詢 (6,10)區間

③在第2層 , 8<9 因此跳到第一層,查詢(8,10)區間

④在第1層, 9=9 找到目標數字

跳躍表的實現

簡讀筆記_Redis設計與實現_第一章_資料結構與物件

Redis中的跳躍表

  1. 跳躍表是有序集合的底層實現之一
  2. Redis的跳躍表實現由zskiplist 和 zskiplistNode兩個結構組成,其中zskiplist用於儲存跳躍表資訊(比如表頭節點 , 表尾節點 , 長度) , 而zskiplistNode用於表示跳躍表節點.
  3. 每個跳躍表節點的層高都是1至32之間隨機數
  4. 在同一個跳躍表中,多個節點可以包含相同的分數,但每個節點的成員物件必須是唯一的.
  5. 當分值相同時,節點按成員物件的字典排序大小進行排序

整數集合

整數集合是集合鍵的底層實現之一,當一個集合只包含整數值元素,並且這個集合的元素數量不多時,Redis就會使用整數集合作為集合鍵的底層實現

簡讀筆記_Redis設計與實現_第一章_資料結構與物件

目的: 儘可能的節約記憶體

  • 可以儲存 int16_t , int32_t , int64_t 三種型別的整數集

  • 整數集合的底層實現為陣列,這個陣列以有序,無重複的方式儲存集合元素

  • 為了節約記憶體, 集合型別使用最小型別儲存整數, 僅當新新增的整數大於當前所能容納的值的範圍時進行升級操作

  • 新增,刪除元素時間複雜度為O(n) , 查詢的時間複雜度為log(n)

  • 不支援降級操作

    升級步驟
    	1. 根據新元素的型別擴充套件底層陣列空間,併為新元素分配空間
    	2. 轉換現有元素至新的型別,保持有序性防止元素
    	3. 新增元素,當新元素小於現有元素時放在索引0, 當新元素大於所有現有元素時放在索引length-1的位置
    複製程式碼

壓縮列表

  • 壓縮列表(ziplist)是列表鍵和雜湊鍵的底層實現之一. 當一個列表鍵只包含少量列表項,並且每個列表項要麼是小整數值,要麼是長度比較短的字串時,那麼Redis會使用壓縮列表來做列表項的底層實現

  • 列表項是Redis為了節約記憶體而開發的,是由一系列特殊編碼的連續記憶體塊組成的順序性資料結構.

壓縮列表的構成

簡讀筆記_Redis設計與實現_第一章_資料結構與物件

壓縮列表:
	zlbytes: 列表總長  zltail:距離表尾偏移量   zllen:節點個數    zlend:特殊值,用於標記壓縮列表末尾
	
壓縮列表節點:
	previous_entry_length:前一項節點的長度(用於從尾向頭遍歷)
    encoding:記錄節點content的型別和長度(位元組or字元)   contents:節點內容
複製程式碼

連鎖更新現象

previous_entry_length用於儲存前一項節點的長度

當前一項長度小於254位元組 該屬性用1位元組表示; 當前一項大於254位元組,該屬性用5位元組表示.

簡讀筆記_Redis設計與實現_第一章_資料結構與物件

  • 插入時連鎖更新

    當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設計與實現_第一章_資料結構與物件

五種物件型別

型別常量 物件名稱
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)

相關文章