Redis資料結構及物件(下)

王老魔發表於2019-05-06

Redis資料結構及物件(下)
  在上一篇文章中分析了一下redis的底層資料結構,這一篇繼續來分析redis的物件,redis的物件都會用到一到三個底層資料結構,redis會在不同的應用場景下采用相應合適的資料結構,以達到平衡時間效率和空間效率的目的。另外redis對於物件還會有型別檢查,記憶體回收等操作以輔助系統的執行。

redisObject結構


實際上每一個redis都是一個redisObject物件。redis物件的型別檢查,記憶體回收,物件共享都是基於redisObject完成的,下面來看一下redisObject的結構。

typedef struct redisObject {
    // 型別
    unsigned type:4;

    // 對齊位
    unsigned notused:2;

    // 編碼方式
    unsigned encoding:4;

    // LRU 時間(相對於 server.lruclock)
    unsigned lru:22;

    // 引用計數
    int refcount;

    // 指向物件的值
    void *ptr;
} robj;
複製程式碼
  • type:物件型別。
  • encoding: 物件編碼方式。
  • lru: 物件的最近使用時間,配合記憶體回收使用。
  • refcount: 引用次數。配合記憶體回收使用。
  • ptr: 指向具體底層資料結構的指標。

redisObject是這樣的結構主要是為了方便redis靈活的切換物件的編碼及實時的回收redis內失效的記憶體,防止記憶體洩漏。

五大型別物件


redis的五大資料物件分別是字串、列表、集合、有序集合、雜湊表,下面來分別介紹。

字串

  字串物件一共有三種實現——整數、SDS(簡單動態字串)和embstr編碼的SDS。   當字串的長度小於44時,會使用embstr編碼的SDS,大於44時會使用SDS。這裡主要有兩個問題,一是什麼是embstr編碼,二是為什麼偏偏要選擇44為臨界點。 <1> embstr編碼   首先需要了解redisObject物件,如下圖所示,一般的string的redisObject在記憶體中是以這樣的形式存在的,需要分配兩塊空間,且要分配兩次。

string在記憶體中的圖示
  而embstr編碼在記憶體中的分佈是這樣的,直接和redisObject的頭部連在一起,只有一塊空間,且只用分配一次記憶體。所以相較於分開兩塊記憶體儲存,embstr編碼更能夠發揮快取的優勢。
embstr

<2> 44位元組   從2.4版本開始,redis開始使用jemalloc記憶體分配器,jemalloc會分配8,16,32,64等位元組的記憶體。embstr由redisObject和sds組成,其中redisObject有16個位元組,如果embstr有44個位元組,則sds的長度為44+3+1=48,加起來剛好為64位元組。

列表

  當列表物件可以同時滿足以下兩個條件時, 列表物件使用 ziplist 編碼:

  • 列表物件儲存的所有字串元素的長度都小於 64 位元組;
  • 列表物件儲存的元素數量小於 512 個;

  當這兩個條件中的一個不滿足時,將使用雙端列表,而且這種變化是動態的。

雜湊表

  當雜湊表物件滿足ziplist編碼的條件時,會使用ziplist作為底層資料結構,當不滿足條件時,會使用字典作為底層資料結構。

雜湊表ziplist斌嗎

集合

  當集合物件可以同時滿足以下兩個條件時, 物件使用 intset 編碼:

  • 集合物件儲存的所有元素都是整數值;
  • 集合物件儲存的元素數量不超過 512 個;   當這兩個條件中的一個不滿足時,將使用hashtable,而且這種變化是動態的。

有序集合

  當集合物件可以同時滿足以下兩個條件時, 物件使用 ziplist編碼:

  • 列表物件儲存的所有字串元素的長度都小於 64 位元組;
  • 列表物件儲存的元素數量小於 512 個;
    有序集合ziplist編碼
      當這兩個條件中的一個不滿足時,將使用跳躍表和hashtable兩種資料結構。
    有序集合使用跳躍表和雜湊表
      為什麼要將資料多冗餘出一份呢?其實單憑跳躍表或雜湊表中的一個資料結構都可以完成有序集合的需求,但是隻有使用雜湊表時才能以O(1)的複雜度查詢成員的分值,但是在使用zrange等範圍性操作時,相比於使用跳躍表,會使得複雜度從O(N),上升到O(NLogN),而光使用跳躍表時,原本的以O(1)的複雜度查詢成員的分值,會變成O(N),為了這兩種型別的操作都能有良好的表現,redis使用了兩種資料結構來實現有序集合。

型別檢查和命令多型


  redis會對命令進行檢查,以確保命令被正確的運用到正確的物件之上,當命令和物件不匹配時,會向客戶端返回一個錯誤,例如當要對一個字串執行自增操作時,就會返回一個錯誤。下表顯示了這種匹配關係。

SET 、 GET 、 APPEND 、 STRLEN 等命令只能對字串鍵執行; HDEL 、 HSET 、 HGET 、 HLEN 等命令只能對雜湊鍵執行; RPUSH 、 LPOP 、 LINSERT 、 LLEN 等命令只能對列表鍵執行; SADD 、 SPOP 、 SINTER 、 SCARD 等命令只能對集合鍵執行; ZADD 、 ZCARD 、 ZRANK 、 ZSCORE 等命令只能對有序集合鍵執行;

  上面說過,redis的物件都是redisObject,型別檢查就是通過redisObject的type屬性完成的。同時依賴於type屬性,redis還可以實現命令的多型,即相同的命令作用的不同的物件上,執行不同的操作。例如當對整數型字串執行append操作時,redis會首先將值轉化為sds,之後執行append操作,而對sds型的字串執行append操作時,則直接執行append操作。

記憶體回收


  redis採用了引用計數法來回收記憶體,在redisObject結構中有一個refCount屬性便是為此服務,當建立物件時,refCount置為1,當物件被引用則refCount+1,不被引用則refCount-1,當refCount為0時,物件所佔用的記憶體就會被回收。

記憶體共享


  redis 會在初始化伺服器時, 建立一萬個字串物件, 這些物件包含了從 0 到 9999 的所有整數值, 當伺服器需要用到值為 0到 9999 的字串物件時, 伺服器就會使用這些共享物件, 而不是新建立物件。

物件空轉時間


  redisObject的lru屬性記錄了物件最後一次被命令程式訪問的時間,此屬性主要是為了配合redis的回收記憶體演算法,例如volatile-lru 或者 allkeys-lru,當記憶體超過設定的maxmemory時,會配合回收演算法計算要被回收的記憶體。

相關文章