今天咱們來看一下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等命令的關鍵。 經過同時使用字典和跳躍表,有序集能夠高效地實現按成員查找和按順序查找兩種操做。