在Redis 的指令中,用于對鍵(key)進行處理的指令占了很大一部分,而對于鍵所儲存的值的
類型(後簡稱“鍵的類型” ),鍵能執行的指令又各不相同。
比如說,LPUSH 和LLEN 隻能用于清單鍵,而SADD 和SRANDMEMBER 隻能用于集合
鍵,等等。
另外一些指令,比如DEL 、TTL 和TYPE ,可以用于任何類型的鍵,但是,要正确實作這些
指令,必須為不同類型的鍵設定不同的處理方式:比如說,删除一個清單鍵和删除一個字元串
鍵的操作過程就不太一樣。
以上的描述說明,Redis 必須讓每個鍵都帶有類型資訊,使得程式可以檢查鍵的類型,并為它
選擇合适的處理方式。
另外,在前面介紹各個底層資料結構時有提到,Redis 的每一種資料類型,比如字元串、清單、
有序集,它們都擁有不隻一種底層實作(Redis 内部稱之為編碼,encoding),這說明,每當對
某種資料類型的鍵進行操作時,程式都必須根據鍵所采取的編碼,進行不同的操作。
比如說,集合類型就可以由字典和整數集合兩種不同的資料結構實作,但是,當使用者執行
ZADD 指令時,他/她應該不必關心集合使用的是什麼編碼,隻要Redis 能按照ZADD 指令的
訓示,将新元素添加到集合就可以了。
這說明,操作資料類型的指令除了要對鍵的類型進行檢查之外,還需要根據資料類型的不同編
碼進行多态處理。
為了解決以上問題,Redis 建構了自己的類型系統,這個系統的主要功能包括:
redisObject 對象。
基于redisObject 對象的類型檢查。
基于redisObject 對象的顯式多态函數。
對redisObject 進行配置設定、共享和銷毀的機制。
以下小節将分别介紹類型系統的這幾個方面。
Note: 因為C 并不是面向對象語言,這裡将redisObject 稱呼為對象一是為了講述的友善,
二是希望通過模仿OOP 的常用術語,讓這裡的内容更容易被了解,redisObject 實際上是隻
是一個結構類型。
redisObject 資料結構,以及Redis 的資料類型
redisObject 是Redis 類型系統的核心,資料庫中的每個鍵、值,以及Redis 本身處理的參數,
都表示為這種資料類型。
redisObject 的定義位于redis.h :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<code>/*</code>
<code>* Redis 對象</code>
<code>*/</code>
<code>typedef</code> <code>struct</code> <code>redisObject {</code>
<code>// 類型</code>
<code>unsigned type:4;</code>
<code>// 對齊位</code>
<code>unsigned notused:2;</code>
<code>// 編碼方式</code>
<code>unsigned encoding:4;</code>
<code>// LRU 時間(相對于server.lruclock)</code>
<code>unsigned lru:22;</code>
<code>// 引用計數</code>
<code>int</code> <code>refcount;</code>
<code>// 指向對象的值</code>
<code>void</code> <code>*ptr;</code>
<code>} robj;</code>
type 、encoding 和ptr 是最重要的三個屬性。
type 記錄了對象所儲存的值的類型,它的值可能是以下常量的其中一個(定義位于redis.h):
<code>* 對象類型</code>
<code>#define REDIS_STRING 0 // 字元串</code>
<code>#define REDIS_LIST 1 // 清單</code>
<code>#define REDIS_SET 2 // 集合</code>
<code>#define REDIS_ZSET 3 // 有序集</code>
<code>#define REDIS_HASH 4 // 哈希表</code>
encoding 記錄了對象所儲存的值的編碼,它的值可能是以下常量的其中一個(定義位于
redis.h):
<code>* 對象編碼</code>
<code>#define REDIS_ENCODING_RAW 0 // 編碼為字元串</code>
<code>#define REDIS_ENCODING_INT 1 // 編碼為整數</code>
<code>#define REDIS_ENCODING_HT 2 // 編碼為哈希表</code>
<code>#define REDIS_ENCODING_ZIPMAP 3 // 編碼為zipmap</code>
<code>#define REDIS_ENCODING_LINKEDLIST 4 // 編碼為雙端連結清單</code>
<code>#define REDIS_ENCODING_ZIPLIST 5 // 編碼為壓縮清單</code>
<code>#define REDIS_ENCODING_INTSET 6 // 編碼為整數集合</code>
<code>#define REDIS_ENCODING_SKIPLIST 7 // 編碼為跳躍表</code>
ptr 是一個指針,指向實際儲存值的資料結構,這個資料結構由type 屬性和encoding 屬性決
定。
舉個例子, 如果一個redisObject 的type 屬性為REDIS_LIST , encoding 屬性為
REDIS_ENCODING_LINKEDLIST ,那麼這個對象就是一個Redis 清單,它的值儲存在一個雙
端連結清單内,而ptr 指針就指向這個雙端連結清單;
另一方面, 如果一個redisObject 的type 屬性為REDIS_HASH , encoding 屬性為
REDIS_ENCODING_ZIPMAP ,那麼這個對象就是一個Redis 哈希表,它的值儲存在一個zipmap
裡,而ptr 指針就指向這個zipmap ;諸如此類。
下圖展示了redisObject 、Redis 所有資料類型、以及Redis 所有編碼方式(底層實作)三者
之間的關系:
<a href="http://s3.51cto.com/wyfs02/M02/12/17/wKiom1L42DPwvSEaAAF7ivDMBUg512.jpg" target="_blank"></a>
這個圖展示了Redis 各種資料類型,以及它們的編碼方式。
Note: REDIS_ENCODING_ZIPMAP 沒有出現在圖中,因為從Redis 2.6 開始,它不再是任何數
據類型的底層結構。
指令的類型檢查和多态
有了redisObject 結構的存在,在執行處理資料類型的指令時,進行類型檢查和對編碼進行多
态操作就簡單得多了。
當執行一個處理資料類型的指令時,Redis 執行以下步驟:
1. 根據給定key ,在資料庫字典中查找和它像對應的redisObject ,如果沒找到,就傳回
NULL 。
2. 檢查redisObject 的type 屬性和執行指令所需的類型是否相符,如果不相符,傳回類
型錯誤。
3. 根據redisObject 的encoding 屬性所指定的編碼,選擇合适的操作函數來處理底層的
資料結構。
4. 傳回資料結構的操作結果作為指令的傳回值。
作為例子,以下展示了對鍵key 執行LPOP 指令的完整過程:
<a href="http://s3.51cto.com/wyfs02/M00/12/17/wKiom1L42Imy4-ZLAAGu7U_h9E8702.jpg" target="_blank"></a>
對象共享
有一些對象在Redis 中非常常見,比如指令的傳回值OK 、ERROR 、WRONGTYPE 等字元,另外,
一些小範圍的整數,比如個位、十位、百位的整數都非常常見。
為了利用這種常見情況,Redis 在内部使用了一個Flyweight 模式:通過預配置設定一些常見的值
對象,并在多個資料結構之間共享這些對象,程式避免了重複配置設定的麻煩,也節約了一些CPU
時間。
Redis 預配置設定的值對象有如下這些:
各種指令的傳回值,比如執行成功時傳回的OK ,執行錯誤時傳回的ERROR ,類型錯誤時
傳回的WRONGTYPE ,指令入隊事務時傳回的QUEUED ,等等。
包括0 在内, 小于redis.h/REDIS_SHARED_INTEGERS 的所有整數
(REDIS_SHARED_INTEGERS 的預設值為10000)
因為指令的回複值直接傳回給用戶端,是以它們的值無須進行共享;另一方面,如果某個指令
的輸入值是一個小于REDIS_SHARED_INTEGERS 的整數對象,那麼當這個對象要被儲存進資料
庫時,Redis 就會釋放原來的值,并将值的指針指向共享對象。
作為例子,下圖展示了三個清單,它們都帶有指向共享對象數組中某個值對象的指針:
<a href="http://s3.51cto.com/wyfs02/M02/12/17/wKioL1L42JjRFnk3AAB-yp0St_A115.jpg" target="_blank"></a>
三個清單的值分别為:
清單A :[20130101, 300, 10086] ,
清單B :[81, 12345678910, 999] ,
清單C :[100, 0, -25, 123] 。
Note: 共享對象隻能被帶指針的資料結構使用。
需要提醒的一點是,共享對象隻能被字典和雙端連結清單這類能帶有指針的資料結構使用。
像整數集合和壓縮清單這些隻能儲存字元串、整數等字面值的記憶體資料結構,就不能使用共享
對象。
引用計數以及對象的銷毀
當将redisObject 用作資料庫的鍵或者值,而不是用來儲存參數時,對象的生命期是非常長
的,因為C 語言本身沒有自動釋放記憶體的相關機制,如果隻依靠程式員的記憶來對對象進行追
蹤和銷毀,基本是不太可能的。
另一方面,正如前面提到的,一個共享對象可能被多個資料結構所引用,這時像是“這個對象被
引用了多少次? ”之類的問題就會出現。
為了解決以上兩個問題,Redis 的對象系統使用了引用計數技術來負責維持和銷毀對象,它的
運作機制如下:
每個redisObject 結構都帶有一個refcount 屬性,訓示這個對象被引用了多少次。
當新建立一個對象時,它的refcount 屬性被設定為1 。
當對一個對象進行共享時,Redis 将這個對象的refcount 增一。
當使用完一個對象之後,或者取消對共享對象的引用之後,程式将對象的refcount 減
一。
當對象的refcount 降至0 時,這個redisObject 結構,以及它所引用的資料結構的内
存,都會被釋放。
小結
Redis 使用自己實作的對象機制來實作類型判斷、指令多态和基于引用計數的垃圾回收。
一種Redis 類型的鍵可以有多種底層實作。
Redis 會預配置設定一些常用的資料對象,并通過共享這些對象來減少記憶體占用,和避免頻繁
地為小對象配置設定記憶體。
本文轉自shayang8851CTO部落格,原文連結:http://blog.51cto.com/janephp/1357861,如需轉載請自行聯系原作者