redis底層設計(三)——redis數據類型

今天咱們來看一下redis的數據類型。既然redis的鍵值對能夠保存不一樣類型的值,那麼很天然就須要對鍵值對的類型進行檢查以及多態處理。下面咱們將對redis所使用的對象系統進行了解,並分別觀察字符串、哈希表、列表、集合和有序集類型的底層實現。redis

3.1 對象處理機制數據庫

  在redis的命令中,用於對鍵進行處理的命令佔了很大一部分,而對於鍵所保存的值的類型(鍵的類型),鍵能執行的命令又各不相同。如:LPUSH和LLEN只能用於列表鍵,而SADD和SRANDMEMBER只能用於集合鍵。又好比DEL、TTL和TRPE能夠用於任何類型的鍵,因此要正確實現這些命令,必須爲不一樣類型的鍵設置不一樣的處理方式;redis的每一種數據類型,好比字符串、列表、有序集,它們都擁有不止一種底層實現,這說明每當對某種數據進行處理的時候,程序必須根據鍵所採起的編碼進行不一樣的操做。服務器

  綜上:操做數據類型的命令除了要對鍵的類型進行檢查以外,還須要根據數據類型的不一樣編碼進行多態處理。網絡

  3.1.1 redisObject 數據結構,以及redis的數據類型數據結構

  redisObject是redis類型系統的核心,數據庫中的每一個鍵、值,以及redis自己處理的函數,都表示爲這種數據類型。函數

/*
* Redis 對象
*/
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和ptr是最重要的三個屬性。編碼

  type記錄了對象所保存的值的類型,它的值多是如下常量中的一個:spa

/*
* 對象類型
*/
#define REDIS_STRING 0 // 字符串
#define REDIS_LIST 1 // 列表
#define REDIS_SET 2 // 集合
#define REDIS_ZSET 3 // 有序集
#define REDIS_HASH 4 // 哈希表

  encoding記錄了對象所保存的值的編碼,它的值多是如下常量中的一個:3d

/*
* 對象編碼
*/
#define REDIS_ENCODING_RAW 0 // 編碼爲字符串
#define REDIS_ENCODING_INT 1 // 編碼爲整數
#define REDIS_ENCODING_HT 2 // 編碼爲哈希表
#define REDIS_ENCODING_ZIPMAP 3 // 編碼爲zipmap
#define REDIS_ENCODING_LINKEDLIST 4 // 編碼爲雙端鏈表
#define REDIS_ENCODING_ZIPLIST 5 // 編碼爲壓縮列表
#define REDIS_ENCODING_INTSET 6 // 編碼爲整數集合
#define REDIS_ENCODING_SKIPLIST 7 // 編碼爲跳躍表

  ptr是一個指針,指向實際保存值的數據結構,這個數據結構由type和encoding屬性決定。舉個例子, 若是一個redisObject 的type 屬性爲REDIS_LIST , encoding 屬性爲REDIS_ENCODING_LINKEDLIST ,那麼這個對象就是一個Redis 列表,它的值保存在一個雙端鏈表內,而ptr 指針就指向這個雙端鏈表;指針

  下圖展現了redisObject 、Redis 全部數據類型、以及Redis 全部編碼方式(底層實現)三者之間的關係:

 

  注意:REDIS_ENCODING_ZIPMAP沒有出如今圖中,由於在redis2.6開始,它再也不是任何數據類型的底層結構。

  3.1.2 命令的類型檢查和多態

  當執行一個處理數據類型命令的時候,redis執行如下步驟:

    1)根據給定的key,在數據庫字典中查找和他相對應的redisObject,若是沒找到,就返回NULL;

    2)檢查redisObject的type屬性和執行命令所需的類型是否相符,若是不相符,返回類型錯誤;

    3)根據redisObject的encoding屬性所指定的編碼,選擇合適的操做函數來處理底層的數據結構;

    4)返回數據結構的操做結果做爲命令的返回值。

  好比如今執行LPOP命令:

  3.1.3 對象共享

  redis通常會把一些常見的值放到一個共享對象中,這樣可以使程序避免了重複分配的麻煩,也節約了一些CPU時間。

  redis預分配的值對象以下:

    1)各類命令的返回值,好比成功時返回的OK,錯誤時返回的ERROR,命令入隊事務時返回的QUEUE,等等

    2)包括0 在內,小於REDIS_SHARED_INTEGERS的全部整數(REDIS_SHARED_INTEGERS的默認值是10000)

  注意:共享對象只能被字典和雙向鏈表這類能帶有指針的數據結構使用。像整數集合和壓縮列表這些只能保存字符串、整數等自勉之的內存數據結構

   3.1.4 引用計數以及對象的消毀:

    * 每一個redisObject結構都帶有一個refcount屬性,指示這個對象被引用了多少次;

    * 當新建立一個對象時,它的refcount屬性被設置爲1;

    * 當對一個對象進行共享時,redis將這個對象的refcount加一;

    * 當使用完一個對象後,或者消除對一個對象的引用以後,程序將對象的refcount減一;

    * 當對象的refcount降至0 時,這個RedisObject結構,以及它引用的數據結構的內存都會被釋放。

  3.1.5 小結:

    * redis使用本身實現的對象機制來實現類型判斷、命令多態和基於引用次數的垃圾回收;

    * redis會預分配一些經常使用的數據對象,並經過共享這些對象來減小內存佔用,和避免頻繁的爲小對象分配內存。

  

3.2 字符串  

  REDIS_STRING(字符串)是redis使用最普遍的數據類型,他除了是set、get等命令的操做對象以外,數據庫中的全部鍵,以及執行命令時提供給redis的參數都是用這種類型保存的。

  3.2.1 字符串編碼:

  字符串類型分別使用REDIS_ENCODING_INT和REDIS_ENCODING_RAW兩種編碼:

    * REDIS_ENCODING_INT使用long類型來保存long類型值;  

    * REDIS_ENCODING_RAW使用sdshdr 結構來保存sds(便是 char*)、long long 、double 和 long double 類型值。

  換句話來講,在redis中,只有能表示爲long類型的值,纔會以整數的形式保存,其餘類型的整數、小數和字符串,都是用sdshdr結構來保存。

 

  新建立的字符串默認使用REDIS_ENCODING_RAW 編碼,在將字符串做爲鍵或者值保存進數據庫時,程序會嘗試將字符串轉爲REDIS_ENCODING_INT 編碼。

  

3.3 哈希表

  REDIS_HASH(哈希表)是HSET、HLEN等命令的操做對象。他使用REDIS_ENCODING_ZIPLIST 和 REDIS_ENCODING_HT 兩種編碼方式:

   

  3.3.1 字典編碼的哈希表:

  哈希表所使用的字典的鍵和值都是字符串對象。

  

  3.3.2 壓縮列表編碼的哈希表:

  當使用REDIS_ENCODING_ZIPLIST 編碼哈希表時,程序經過將鍵和值一同推入壓縮列表,從而造成保存哈希表所需的鍵-值對結構:

 

  新添加的key-value會被添加到壓縮列表的表尾。當進行查找/刪除或更新操做時,程序先定位到鍵的位置,而後再經過對鍵的位置來定位值的位置。

  建立空白哈希表時,程序默認使用REDIS_ENCODING_ZIPLIST 編碼,當如下任何一個條件被知足時,程序將編碼從切換爲REDIS_ENCODING_HT :
    • 哈希表中某個鍵或某個值的長度大於server.hash_max_ziplist_value (默認值爲64)。
    • 壓縮列表中的節點數量大於server.hash_max_ziplist_entries (默認值爲512 )。

 

3.4 列表

  REDIS_LIST(列表)是LPUSH、LRANGE等命令的操做對象,他使用REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLIST這兩種方式編碼:

 

  3.4.1 編碼的選擇:   

  建立新列表時Redis 默認使用REDIS_ENCODING_ZIPLIST 編碼,當如下任意一個條件被知足時,列表會被轉換成REDIS_ENCODING_LINKEDLIST 編碼:
    • 試圖往列表新添加一個字符串值, 且這個字符串的長度超過server.list_max_ziplist_value (默認值爲64 )。
    • ziplist 包含的節點超過server.list_max_ziplist_entries (默認值爲512 )。

  3.4.2 阻塞的條件:  

  BLPOP、LRPOP和BRPOPLPUSH三個命令均可能形成客戶端被阻塞,因此咱們將這些命令統稱爲列表的阻塞原語。

  阻塞原語並非必定會形成客戶端阻塞:
    • 只有當這些命令被用於空列表時,它們纔會阻塞客戶端。
    • 若是被處理的列表不爲空的話,它們就執行無阻塞版本的LPOP 、RPOP 或RPOPLPUSH命令。

  以下:

  3.4.3 阻塞的過程:

  當一個阻塞原語的處理目標爲空值時,執行該阻塞原語的客戶端就會被阻塞。阻塞一個客戶端須要執行如下步驟:

    1)將客戶端的狀態設置爲「正在阻塞」,並記錄阻塞這個客戶端的各個鍵,以及阻塞的最長時限(timeout)等數據;

    2)將客戶端的信息記錄到server.db[i]->blocking_keys中(其中i爲客戶端所使用的數據庫號碼);

    3)繼續維持客戶端和服務器之間的網絡鏈接,但再也不向客戶端傳送任何信息,形成客戶端阻塞。

   步驟2 是未來解除阻塞的關鍵,server.db[i]->blocking_keys 是一個字典,字典的鍵是那些形成客戶端阻塞的鍵,而字典的值是一個鏈表,鏈表裏保存了全部由於這個鍵而被阻塞的客戶端(被同一個鍵所阻塞的客戶端可能不止一個):

 

  當客戶端被阻塞後,脫離阻塞狀態有如下3種方法:

    1)被動脫離:有其餘客戶端爲形成阻塞的鍵推入了新元素;

    2)主動脫離:到達執行阻塞原語時設定的最大阻塞時間;

    3)強制脫離:客戶端強制終止和服務器的鏈接,或者服務器停機。

 

  3.4.4 阻塞因LPUSH、RPUSH、LINSERT等添加命令而被取消

  經過將新元素推入形成客戶端阻塞的某個鍵中,可讓相應的客戶端從阻塞狀態中脫離出來(取消阻塞的客戶端數量取決於推入元素的數量);這3個添加元素命令在底層實現上都是pushGenericCommand函數去執行的。

  

  當向一個空鍵推入新元素時,pushGenericCommand 函數執行如下兩件事:
    1. 檢查這個鍵是否存在於前面提到的server.db[i]->blocking_keys 字典裏,若是是的話,那麼說明有至少一個客戶端由於這個key 而被阻塞,程序會爲這個鍵建立一個redis.h/readyList 結構,並將它添加到server.ready_keys 鏈表中。
    2. 將給定的值添加到列表鍵中。

  readyList的結構以下:

typedef struct readyList {
redisDb *db;
robj *key;
} readyList;

  key屬性指向形成阻塞的鍵,而db則指向該鍵所在的數據庫。

  好比說:假設某個非阻塞客戶端正在使用0 號數據庫,而這個數據庫當前的blocking_keys屬性的值以下:

  

  若是這時客戶端對該數據庫執行PUSH key3 value ,那麼pushGenericCommand 將建立一個db 屬性指向0 號數據庫、key 屬性指向key3 鍵對象的readyList 結構,並將它添加到服務器server.ready_keys 屬性的鏈表中:

 

  此時pushGenericCommand 函數完成了如下兩件事:

    1)將readyList添加到服務器;

    2)將新元素value添加到鍵key3;

  雖然key3已經再也不是空鍵,但到目前爲止,被key3阻塞的客戶端尚未任何一個唄解除阻塞狀態。這時redis會調用handleClientsBlockedOnLists函數,執行步驟以下: 

    1. 若是server.ready_keys 不爲空, 那麼彈出該鏈表的表頭元素, 並取出元素中的readyList 值。
    2. 根據readyList 值所保存的key 和db ,在server.blocking_keys 中查找全部由於key而被阻塞的客戶端(以鏈表的形式保存)。
    3. 若是key 不爲空,那麼從key 中彈出一個元素,並彈出客戶端鏈表的第一個客戶端,而後將被彈出元素返回給被彈出客戶端做爲阻塞原語的返回值。
    4. 根據readyList 結構的屬性,刪除server.blocking_keys 中相應的客戶端數據,取消客戶端的阻塞狀態。 

    5. 繼續執行步驟3 和4 ,直到key 沒有元素可彈出,或者全部由於key 而阻塞的客戶端都取消阻塞爲止。
    6. 繼續執行步驟1 ,直到ready_keys 鏈表裏的全部readyList 結構都被處理完爲止。

   3.4.5 先阻塞先服務(FBFS)策略

         值得一提的是,當程序添加一個新的被阻塞客戶端到server.blocking_keys 字典的鏈表中時,它將該客戶端放在鏈表的最後,而當handleClientsBlockedOnLists 取消客戶端的阻塞時,它從鏈表的最前面開始取消阻塞:這個鏈表造成了一個FIFO 隊列,最早被阻塞 的客戶端總值最早脫離阻塞狀態,Redis 文檔稱這種模式爲先阻塞先服務(FBFS,first-block-first-serve)。舉個例子,在下圖所示的阻塞情況中,若是客戶端對數據庫執行PUSH key3 value ,那麼只有client3 會被取消阻塞,client6 和client4 仍然阻塞;若是客戶端對數據庫執行PUSH key3 value1 value2 ,那麼client3 和client4 的阻塞都會被取消,而客戶端client6 依然處於阻塞狀態:

 

  3.4.6 阻塞因超過最大等待時間而被取消

    每次Redis 服務器常規操做函數(server cron job)執行時,程序都會檢查全部鏈接到服務器的客戶端,查看那些處於「正在阻塞」狀態的客戶端的最大阻塞時限是否已通過期,若是是的話,就給客戶端返回一個空白回覆,而後撤銷對客戶端的阻塞。

 

3.5 集合

  REDIS_SET(集合) 是SADD。SRANGMEMBER等命令的操做對象,它使用REDIS_ENCODING_INTSET和REDIS_ENCODING_HT兩種方式編碼:

 

  3.5.1 編碼的選擇:

  第一個添加到集合的元素,決定了建立集合時所使用的編碼:
    • 若是第一個元素能夠表示爲long long 類型值(也便是,它是一個整數),那麼集合的初始編碼爲REDIS_ENCODING_INTSET 。
    • 不然,集合的初始編碼爲REDIS_ENCODING_HT 。

   3.5.2 編碼的切換: 

  若是一個集合使用REDIS_ENCODING_INTSET 編碼,那麼當如下任何一個條件被知足時,這個集合會被轉換成REDIS_ENCODING_HT 編碼:
    • intset 保存的整數值個數超過server.set_max_intset_entries (默認值爲512 )。
    • 試圖往集合裏添加一個新元素,而且這個元素不能被表示爲long long 類型(也便是,它不是一個整數)。

   3.5.3 字典編碼的集合:

  當使用REDIS_ENCODING_HT編碼時,集合將元素保存到字典的鍵裏面,而字典的值則統一設爲null,以下集合的成員分別是:elem一、elem2和elem3:

 

3.6 有序集

  REDIS_ZSET(有序集)是ZADD、ZCOUNT等命令的操做對象,它使用REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_SKIPLIST兩種編碼方式:

  3.6.1 編碼的選擇:  

  在經過ZADD 命令添加第一個元素到空key 時,程序經過檢查輸入的第一個元素來決定該建立什麼編碼的有序集。若是第一個元素符合如下條件的話,就建立一個REDIS_ENCODING_ZIPLIST 編碼的有序集:
    • 服務器屬性server.zset_max_ziplist_entries 的值大於0 (默認爲128 )。
    • 元素的member 長度小於服務器屬性server.zset_max_ziplist_value 的值(默認爲64)。不然,程序就建立一個REDIS_ENCODING_SKIPLIST 編碼的有序集。

  3.6.2 編碼的裝換:  

  對於一個REDIS_ENCODING_ZIPLIST 編碼的有序集,只要知足如下任一條件,就將它轉換爲REDIS_ENCODING_SKIPLIST 編碼:
    • ziplist 所保存的元素數量超過服務器屬性server.zset_max_ziplist_entries 的值(默認值爲128 )
    • 新添加元素的member 的長度大於服務器屬性server.zset_max_ziplist_value 的值(默認值爲64 )

   3.6.3 ZIPLIST編碼的有序集

  每一個有序集元素以兩個相鄰的ziplist節點表示,第一個節點保存元素的member域,第二個節點保存元素的score值;多個元素之間按score值從小到大排序,若是兩個元素的score值相同,那麼就按字典對member進行對比,決定哪一個元素排在前面,哪一個元素排在後面

  3.6.4 SKIPLIST編碼的有序集

  當使用REDIS_ENCODING_SKIPLIST編碼時,有序集元素由redis.h/zset 結構來保存

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

  zset同時使用字典和跳躍表兩個數據結構來保存有序集元素。

  其中,元素的成員由一個redisObject 結構表示,而元素的score 則是一個double 類型的浮點數,字典和跳躍表兩個結構經過將指針共同指向這兩個值來節約空間(不用每一個元素都複製兩份)。

  

 

  經過使用字典結構,並將member 做爲鍵,score 做爲值,有序集能夠在O(1) 複雜度內:    • 檢查給定member 是否存在於有序集(被不少底層函數使用);    • 取出member 對應的score 值(實現ZSCORE 命令)。  另外一方面,經過使用跳躍表,可讓有序集支持如下兩種操做:    • 在O(logN) 指望時間、O(N) 最壞時間內根據score 對member 進行定位(被不少底層函數使用);    • 範圍性查找和處理操做,這是(高效地)實現ZRANGE 、ZRANK 和ZINTERSTORE等命令的關鍵。  經過同時使用字典和跳躍表,有序集能夠高效地實現按成員查找和按順序查找兩種操做。

相關文章
相關標籤/搜索