1. 程式人生 > 實用技巧 >5.Redis詳解(五)------ redis的五大資料型別實現原理

5.Redis詳解(五)------ redis的五大資料型別實現原理

前面兩篇部落格,第一篇介紹了五大資料型別的基本用法,第二篇介紹了Redis底層的六種資料結構。在Redis中,並沒有直接使用這些資料結構來實現鍵值對資料庫,而是基於這些資料結構建立了一個物件系統,這些物件系統也就是前面說的五大資料型別,每一種資料型別都至少用到了一種資料結構。通過這五種不同型別的物件,Redis可以在執行命令之前,根據物件的型別判斷一個物件是否可以執行給定的命令,而且可以針對不同的場景,為物件設定多種不同的資料結構,從而優化物件在不同場景下的使用效率。

1、物件的型別與編碼

Redis使用前面說的五大資料型別來表示鍵和值,每次在Redis資料庫中建立一個鍵值對時,至少會建立兩個物件,一個是鍵物件,一個是值物件,而Redis中的每個物件都是由 redisObject 結構來表示:

typedef struct redisObject{
     //型別
     unsigned type:4;
     //編碼
     unsigned encoding:4;
     //指向底層資料結構的指標
     void *ptr;
     //引用計數
     int refcount;
     //記錄最後一次被程式訪問的時間
     unsigned lru:22;
 
}robj

①、type屬性

物件的type屬性記錄了物件的型別,這個型別就是前面講的五大資料型別:

可以通過如下命令來判斷物件型別:

type key

如下圖:

注意:在Redis中,鍵總是一個字串物件,而值可以是字串、列表、集合等物件,所以我們通常說的鍵為字串鍵,表示的是這個鍵對應的值為字串物件,我們說一個鍵為集合鍵時,表示的是這個鍵對應的值為集合物件。

②、encoding 屬性和 *prt 指標

  物件的 prt 指標指向物件底層的資料結構,而資料結構由 encoding 屬性來決定。

  

  而每種型別的物件都至少使用了兩種不同的編碼:

  

  可以通過如下命令檢視值物件的編碼:

OBJECT ENCODING    key 

比如 string 型別:(可以是 embstr編碼的簡單字串或者是 int 整數值實現)

2、字串物件

字串是Redis最基本的資料型別,不僅所有key都是字串型別,其它幾種資料型別構成的元素也是字串。注意字串的長度不能超過512M。

①、編碼

字串物件的編碼可以是int,raw或者embstr。

1、int 編碼:儲存的是可以用 long 型別表示的整數值。

2、raw 編碼:儲存長度大於44位元組的字串(redis3.2版本之前是39位元組,之後是44位元組)。

3、embstr 編碼:儲存長度小於44位元組的字串(redis3.2版本之前是39位元組,之後是44位元組)。

  

  由上可以看出,int 編碼是用來儲存整數值,raw編碼是用來儲存長字串,而embstr是用來儲存短字串。其實 embstr 編碼是專門用來儲存短字串的一種優化編碼,raw 和 embstr 的區別:

  

  

  embstr與raw都使用redisObject和sds儲存資料,區別在於,embstr的使用只分配一次記憶體空間(因此redisObject和sds是連續的),而raw需要分配兩次記憶體空間(分別為redisObject和sds分配空間)。因此與raw相比,embstr的好處在於建立時少分配一次空間,刪除時少釋放一次空間,以及物件的所有資料連在一起,尋找方便。而embstr的壞處也很明顯,如果字串的長度增加需要重新分配記憶體時,整個redisObject和sds都需要重新分配空間,因此redis中的embstr實現為只讀。

ps:Redis中對於浮點數型別也是作為字串儲存的,在需要的時候再將其轉換成浮點數型別。

②、編碼的轉換

當 int 編碼儲存的值不再是整數,或大小超過了long的範圍時,自動轉化為raw。

對於 embstr 編碼,由於 Redis 沒有對其編寫任何的修改程式(embstr 是隻讀的),在對embstr物件進行修改時,都會先轉化為raw再進行修改,因此,只要是修改embstr物件,修改後的物件一定是raw的,無論是否達到了44個位元組。

3、列表物件

list 列表,它是簡單的字串列表,按照插入順序排序,你可以新增一個元素到列表的頭部(左邊)或者尾部(右邊),它的底層實際上是個連結串列結構。

①、編碼

列表物件的編碼可以是 ziplist(壓縮列表) 和 linkedlist(雙端連結串列)。關於連結串列和壓縮列表的特性可以看我前面的這篇部落格

比如我們執行以下命令,建立一個 key = ‘numbers’,value = ‘1 three 5’ 的三個值的列表。

rpush numbers 1 "three" 5

ziplist 編碼表示如下:

  

linkedlist表示如下:

    

②、編碼轉換

當同時滿足下面兩個條件時,使用ziplist(壓縮列表)編碼:

1、列表儲存元素個數小於512個

2、每個元素長度小於64位元組

不能滿足這兩個條件的時候使用 linkedlist 編碼。

上面兩個條件可以在redis.conf 配置檔案中的 list-max-ziplist-value選項和 list-max-ziplist-entries 選項進行配置。

4、雜湊物件

雜湊物件的鍵是一個字串型別,值是一個鍵值對集合。

①、編碼

雜湊物件的編碼可以是 ziplist 或者 hashtable。

當使用ziplist,也就是壓縮列表作為底層實現時,新增的鍵值對是儲存到壓縮列表的表尾。比如執行以下命令:

hset profile name "Tom"
hset profile age 25
hset profile career "Programmer"

如果使用ziplist,profile 儲存如下:

  

  當使用 hashtable 編碼時,上面命令儲存如下:

  

  

hashtable 編碼的雜湊表物件底層使用字典資料結構,雜湊物件中的每個鍵值對都使用一個字典鍵值對。

在前面介紹壓縮列表時,我們介紹過壓縮列表是Redis為了節省記憶體而開發的,是由一系列特殊編碼的連續記憶體塊組成的順序型資料結構,相對於字典資料結構,壓縮列表用於元素個數少、元素長度小的場景。其優勢在於集中儲存,節省空間。

②、編碼轉換

和上面列表物件使用 ziplist 編碼一樣,當同時滿足下面兩個條件時,使用ziplist(壓縮列表)編碼:

1、列表儲存元素個數小於512個

2、每個元素長度小於64位元組

不能滿足這兩個條件的時候使用 hashtable 編碼。第一個條件可以通過配置檔案中的 set-max-intset-entries 進行修改。

5、集合物件

  

集合物件 set 是string 型別(整數也會轉換成string型別進行儲存)的無序集合。注意集合和列表的區別:集合中的元素是無序的,因此不能通過索引來操作元素;集合中的元素不能有重複。

①、編碼

集合物件的編碼可以是 intset 或者 hashtable。

intset 編碼的集合物件使用整數集合作為底層實現,集合物件包含的所有元素都被儲存在整數集合中。

hashtable 編碼的集合物件使用 字典作為底層實現,字典的每個鍵都是一個字串物件,這裡的每個字串物件就是一個集合中的元素,而字典的值則全部設定為 null。這裡可以類比Java集合中HashSet 集合的實現,HashSet 集合是由 HashMap 來實現的,集合中的元素就是 HashMap 的key,而 HashMap 的值都設為 null。

SADD numbers 1 3 5

如圖:

SADD Dfruits "apple" "banana" "cherry"

如圖:

②、編碼轉換

  

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

1、集合物件中所有元素都是整數

2、集合物件所有元素數量不超過512  

不能滿足這兩個條件的就使用 hashtable 編碼。第二個條件可以通過配置檔案的 set-max-intset-entries 進行配置。

6、有序集合物件

  

和上面的集合物件相比,有序集合物件是有序的。與列表使用索引下標作為排序依據不同,有序集合為每個元素設定一個分數(score)作為排序依據。

①、編碼

有序集合的編碼可以是 ziplist 或者 skiplist。  

ziplist 編碼的有序集合物件使用壓縮列表作為底層實現,每個集合元素使用兩個緊挨在一起的壓縮列表節點來儲存,第一個節點儲存元素的成員,第二個節點儲存元素的分值。並且壓縮列表內的集合元素按分值從小到大的順序進行排列,小的放置在靠近表頭的位置,大的放置在靠近表尾的位置。

ZADD price 8.5 apple 5.0 banana 6.0 cherry

如圖:

圖2:

skiplist 編碼的有序集合物件使用 zet 結構作為底層實現,一個 zset 結構同時包含一個字典和一個跳躍表:

typedef struct zset{
     //跳躍表
     zskiplist *zsl;
     //字典
     dict *dice;
} zset;

字典的鍵儲存元素的值,字典的值則儲存元素的分值;跳躍表節點的 object 屬性儲存元素的成員,跳躍表節點的 score 屬性儲存元素的分值。

  

這兩種資料結構會通過指標來共享相同元素的成員和分值,所以不會產生重複成員和分值,造成記憶體的浪費。

說明:其實有序集合單獨使用字典或跳躍表其中一種資料結構都可以實現,但是這裡使用兩種資料結構組合起來,原因是假如我們單獨使用 字典,雖然能以 O(1) 的時間複雜度查詢成員的分值,但是因為字典是以無序的方式來儲存集合元素,所以每次進行範圍操作的時候都要進行排序;假如我們單獨使用跳躍表來實現,雖然能執行範圍操作,但是查詢操作有 O(1)的複雜度變為了O(logN)。因此Redis使用了兩種資料結構來共同實現有序集合。

②、編碼轉換

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

1、儲存的元素數量小於128;

2、儲存的所有元素長度都小於64位元組。

不能滿足上面兩個條件的使用 skiplist 編碼。以上兩個條件也可以通過Redis配置檔案zset-max-ziplist-entries 選項和 zset-max-ziplist-value 進行修改。

7、五大資料型別的應用場景

  

對於string 資料型別,因為string 型別是二進位制安全的,可以用來存放圖片,視訊等內容,另外由於Redis的高效能讀寫功能,而string型別的value也可以是數字,可以用作計數器(INCR,DECR),比如分散式環境中統計系統的線上人數,秒殺等。

對於 hash 資料型別,value 存放的是鍵值對,比如可以做單點登入存放使用者資訊。

對於 list 資料型別,可以實現簡單的訊息佇列,另外可以利用lrange命令,做基於redis的分頁功能

對於 set 資料型別,由於底層是字典實現的,查詢元素特別快,另外set 資料型別不允許重複,利用這兩個特性我們可以進行全域性去重,比如在使用者註冊模組,判斷使用者名稱是否註冊;另外就是利用交集、並集、差集等操作,可以計算共同喜好,全部的喜好,自己獨有的喜好等功能。

對於 zset 資料型別,有序的集合,可以做範圍查詢,排行榜應用,取 TOP N 操作等。

8、記憶體回收和記憶體共享

①、記憶體回收

前面講 Redis 的每個物件都是由 redisObject 結構表示:

typedef struct redisObject{
     //型別
     unsigned type:4;
     //編碼
     unsigned encoding:4;
     //指向底層資料結構的指標
     void *ptr;
     //引用計數
     int refcount;
     //記錄最後一次被程式訪問的時間
     unsigned lru:22;
 
}robj

其中關鍵的 type屬性,encoding 屬性和 ptr 指標都介紹過了,那麼 refcount 屬性是幹什麼的呢?

  因為 C 語言不具備自動回收記憶體功能,那麼該如何回收記憶體呢?於是 Redis自己構建了一個記憶體回收機制,通過在 redisObject 結構中的 refcount 屬性實現。這個屬性會隨著物件的使用狀態而不斷變化:

1、建立一個新物件,屬性 refcount 初始化為1

2、物件被一個新程式使用,屬性 refcount 加 1

3、物件不再被一個程式使用,屬性 refcount 減 1

4、當物件的引用計數值變為 0 時,物件所佔用的記憶體就會被釋放。

  

在 Redis 中通過如下 API 來實現:

  

  

學過Java的應該知道,引用計數的記憶體回收機制其實是不被Java採用的,因為不能克服迴圈引用的例子(比如 A 具有 B 的引用,B 具有 C 的引用,C 具有 A 的引用,除此之外,這三個物件沒有任何用處了),這時候 A B C 三個物件會一直駐留在記憶體中,造成記憶體洩露。那麼 Redis 既然採用引用計數的垃圾回收機制,如何解決這個問題呢?

  

在前面介紹 redis.conf 配置檔案時,在MEMORY MANAGEMENT 下有個 maxmemory-policy 配置:

maxmemory-policy :當記憶體使用達到最大值時,redis使用的清楚策略。有以下幾種可以選擇:

1volatile-lru   利用LRU演算法移除設定過過期時間的key (LRU:最近使用 Least Recently Used ) 

2)allkeys-lru   利用LRU演算法移除任何key 

3volatile-random 移除設定過過期時間的隨機key 

4)allkeys-random  移除隨機key

5volatile-ttl   移除即將過期的key(minor TTL) 

6)noeviction  noeviction   不移除任何key,只是返回一個寫錯誤 ,預設選項

通過這種配置,也可以對記憶體進行回收。

②、記憶體共享

  

refcount 屬性除了能實現記憶體回收以外,還能用於記憶體共享。

  

比如通過如下命令 set k1 100,建立一個鍵為 k1,值為100的字串物件,接著通過如下命令 set k2 100 ,建立一個鍵為 k2,值為100 的字串物件,那麼 Redis 是如何做的呢?

  

1、將資料庫鍵的值指標指向一個現有值的物件

2、將被共享的值物件引用refcount 加 1

  

注意:Redis的共享物件目前只支援整數值的字串物件。之所以如此,實際上是對記憶體和CPU(時間)的平衡:共享物件雖然會降低記憶體消耗,但是判斷兩個物件是否相等卻需要消耗額外的時間。對於整數值,判斷操作複雜度為O(1);對於普通字串,判斷複雜度為O(n);而對於雜湊、列表、集合和有序集合,判斷的複雜度為O(n^2)。

雖然共享物件只能是整數值的字串物件,但是5種類型都可能使用共享物件(如雜湊、列表等的元素可以使用)。

9、物件的空轉時長

在 redisObject 結構中,前面介紹了 type、encoding、ptr 和 refcount 屬性,最後一個 lru 屬性,該屬性記錄了物件最後一次被命令程式訪問的時間。  

使用 OBJECT IDLETIME 命令可以列印給定鍵的空轉時長,通過將當前時間減去值物件的 lru 時間計算得到。

  

  

lru 屬性除了計算空轉時長以外,還可以配合前面記憶體回收配置使用。如果Redis打開了maxmemory選項,且記憶體回收演算法選擇的是volatile-lru或allkeys—lru,那麼當Redis記憶體佔用超過maxmemory指定的值時,Redis會優先選擇空轉時間最長的物件進行釋放。

參考文章:《Redis設計與實現》

轉自:https://www.cnblogs.com/ysocean/p/9102811.html