Redis進階篇(二):Redis對象的底層實現

0. 五類對象分別是什麼

五類對象就是我們常用的string、list、set、zset、hash

1. 爲什麼要有對象

我們平時主要是通過操作對象的api來操作redis,而不是通過它的調用它底層數據結構來完成(外觀模式)。但我們還需要了解其底層,只有這樣才能寫最優化高效的代碼。

  1. 跟java一樣,對象使開發更方便簡潔,降低開發門檻。開發者不需要了解其複雜的底層API,直接調用高層接口即可實現開發。
  2. Redis根據對象類型來判斷命令是否違法,如果你set key value1 value2就報錯。
  3. 對象下可以包含多種數據結構,使數據存儲更加多態化。(下面主講)
  4. Reids基於對象做了垃圾回收(引用計數法)。
  5. 對象帶有更豐富的屬性,來幫助redis實現更高級的功能。(比如對象的閒置時間)。

2. Redis對象(RedisObject)源碼分析

typedef struct redisObject {

    // 類型
    unsigned type:4;

    // 編碼
    unsigned encoding:4;

    // 指向底層實現數據結構的指針
    void *ptr;

    // ...

} robj;

type字段

記錄對象類型。

我們平時用的命令type <key>,其實就是返回這個字段的屬性。

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> type hello
string
127.0.0.1:6379> rpush list 1 2 3
(integer) 3
127.0.0.1:6379> type list
list
...

那type有多少中類型呢?看下面這個表:

對象 type 字段 TYPE命令的輸出
字符串對象 REDIS_STRING "string"
列表對象 REDIS_LIST
哈希對象 REDIS_HASH
集合對象 REDIS_SET
有序集合對象 REDIS_ZSET

encoding字段

記錄對象使用的編碼(數據結構),Reids中稱數據結構爲encoding。

我們可以這樣查看我們redis對象中的encoding:

127.0.0.1:6379> object encoding hello
"embstr"
127.0.0.1:6379> object encoding list
"quicklist"
...

既然它是標明該redisObject是使用的什麼數據結構,那肯定也有個對應的表:

類型 編碼 對象
REDIS_STRING REDIS_ENCODING_INT 使用整數值實現的字符串對象。
REDIS_STRING REDIS_ENCODING_EMBSTR 使用 embstr 編碼的簡單動態字符串實現的字符串對象。
REDIS_STRING REDIS_ENCODING_RAW 使用簡單動態字符串實現的字符串對象。
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的列表對象。
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用雙端鏈表實現的列表對象。
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的哈希對象。
REDIS_HASH REDIS_ENCODING_HT 使用字典實現的哈希對象。
REDIS_SET REDIS_ENCODING_INTSET 使用整數集合實現的集合對象。
REDIS_SET REDIS_ENCODING_HT 使用字典實現的集合對象。
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的有序集合對象。
REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳躍表和字典實現的有序集合對象。

我們可以看到,Redis對對象的底層encoding分的很細,String類型就有三個,其它四個對象都分別有兩種不同的底層數據結構的實現。他們有一規律,就是用ziplistintsetembstr來實現少量的數據,數據量一旦龐大,就會升級到skiplistrawlinkedlistht來實現,後面我會仔細講解。

3. 分別分析各個對象的底層編碼實現(數據結構)

3.1 字符串(string)

字符串編碼有三個:int、raw、embstr。

3.1.1 int

當string對象的值全部是數字,就會使用int編碼。

127.0.0.1:6379> set number 123455
OK
127.0.0.1:6379> object encoding number
"int"
3.1.2 embstr

字符串或浮點數長度小於等於39字節,就會使用embstr編碼方式來存儲,embstr存儲內存一般很小,所以redis一次性分配且內存連續(效率高)。

127.0.0.1:6379> set shortStr "suwe suwe suwe"
OK
127.0.0.1:6379> object encoding shortStr
"embstr"
3.1.2 raw

當一個字符串或浮點數長度大於39字節,就使用SDS來保存,編碼爲raw,由於不確定值的字節大小,所以鍵和值各分配各的,所以就分配兩次內存(回收也是兩次),同理它一定不是內存連續的。

127.0.0.1:6379> set longStr "hello everyone, we dont need to sleep around to go aheard! do you think?"
OK
127.0.0.1:6379> object encoding longStr
"raw"
3.1.3 編碼轉換

前面說過,Redis會自動對編碼進行轉換來適應和優化數據的存儲。

int->raw

條件:數字對象進行append字母,就會發生轉換。

127.0.0.1:6379> object encoding number
"int"
127.0.0.1:6379> append number " is a lucky number"
(integer) 24
127.0.0.1:6379> object encoding number
"raw"

embstr->raw

條件:對embstr進行修改,redis會先將其轉換成raw,然後才進行修改。所以embstr實際上是隻讀性質的。

127.0.0.1:6379> object encoding shortStr
"embstr"
127.0.0.1:6379> append shortStr "(hhh"
(integer) 18
127.0.0.1:6379> object encoding shortStr
"raw"

3.2 列表(list)

列表對象編碼可以是:ziplist或linkedlist。

  1. ziplist壓縮列表不知道大家還記得不,就是zlbytes zltail zllen entry1 entry2 ..end結構,entry節點裏有pre-length、encoding、content屬性,忘記的可以返回去看下。

  2. linkedlist,類似雙向鏈表,也是上一章的知識。

3.2.1 編碼轉換

ziplist->linkedlist

條件:列表對象的所有字符串元素的長度大於等於64字節 & 列表元素數大於等於512. 反之,小於64和小於512會使用ziplist而不是用linkedlist。

這個閾值是可以修改的,修改選項:list-max-ziplist-valuelist-max-ziplist-entriess

3.3 哈希(hash)

哈希對象的編碼有:ziplist和hashtable

3.3.1 編碼轉換

ziplist->hashtable

條件:哈希對象所有鍵和值字符串長度大於等於64字節 & 鍵值對數量大於等於512

這個閾值也是可以修改的,修改選項:hash-max-ziplist-valuehash-max-ziplist-entriess

3.4. 集合(set)

集合對象的編碼有:intset和hashtable

3.4.1 intset
  1. 集合對象所有元素都是整數
  2. 集合對象元素數不超過512個
3.4.2 編碼轉換

intset->hashtable

條件:元素不都是整數 & 元素數大於等於512

3.5. 有序集合(zset)

有序集合用到的編碼:ziplist和skiplist

大家可能很好奇阿,ziplist的entry中只有屬性content可以存放數據,集合也是key-value形式,那怎麼存儲呢?

第一個節點保存key、第二個節點保存value 以此類推...

3.5.1 爲什麼要用這兩個編碼
  1. 如果只用ziplist來實現,無法做到元素的排序,不支持範圍查找,能做到元素的快速查找。
  2. 如果只用skiplist來實現,無法做到快速查找,但能做到元素排序、範圍操作。
3.5.2 編碼轉換

ziplist->skiplist

條件:有序集合元素數 >= 128 & 含有元素的長度 >= 64

這個閾值也是可以修改的,修改選項:zset-max-ziplist-valuezset-max-ziplist-entriess

4. 垃圾回收

爲什麼要說內存回收呢,因爲redisObject有一個字段:

typedef struct redisObject {

    // ...

    // 引用計數
    int refcount;

    // ...

} robj;

redis的垃圾回收採用引用計數法(和jvm一樣),底層採用一個變量對對象的使用行爲進行計數。

  • 初始化爲1
  • 對象被引用,+1
  • 對象引用消除,-1
  • 計數器==0, 回收對象

5. 對象共享

5.1 對象共享的體現

  1. redis中,值是整數值且相等的兩個對象,redis會將該對象進行共享,且引用計數+1
  2. redis啓動會自動生成0-9999的整數值放到內存中來共享。

5.2 爲什麼要對象共享

節約內存

5.3 爲什麼不對字符串進行共享

成本太高。

驗證整數相等只需要O(1)的時間複雜度,而驗證字符串要O(n).

6. 對象的空閒時長

最後,redisObject還有一個字段,記錄了對象最後一次被訪問的時間:

typedef struct redisObject {

    // ...

    unsigned lru:22;

    // ...

} robj;

因爲這個字段記錄對象最後一次被訪問的時間,所以它可以用來查看該對象多久未使用,即:用當前時間-lru

127.0.0.1:6379> object idletime hello
(integer) 5110

它還關係到redis的熱點數據實現,如果我們選擇lr算法,當內存超出閾值後會對空閒時長較高的對象進行釋放,回收內存。


關注我的公衆號,隨時閱讀我的全部文章。


想看往期文章, 請點擊我的GitHub地址: https://github.com/fantj2016/java-reader



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章