Redis數據類型內部編碼規則及優化方式


Redis的每個鍵值都是使用一個redisObject結構體保存的,redisObject的定義如下:

typedef struct redisObject {

unsigned type:4;

unsigned notused:2; /* Not used */

unsigned encoding:4;

unsigned lru:22;  /* lru time (relative to server.lruclock) */

int refcount;

void *ptr;

} robj;

1.字符串類型

Redis使用一個sdshdr類型的變量來存儲字符串,而redisObject的ptr字段指向的是該變量的地址。sdshdr的定義如下:

struct sdshdr {

int len;
int free;
char buf[];

};

其中len字段表示的是字符串的長度,free字段表示buf中的剩余空間,而buf字段存儲的才是字符串的內容。

所以當執行 SET key foobar時,存儲鍵值需要占用的空間是 sizeof(redisObject) + sizeof(sdshdr) + strlen("foobar") = 30字節[8] ,如圖4-4所示。

而當鍵值內容可以用一個64位有符號整數表示時,Redis會將鍵值轉換成long類型來存儲。如 SET key 123456,實際占用的空間是 sizeof(redisObject) = 16 字節,比存儲"foobar"節省了一半的存儲空間,如圖4-5所示。

圖4-4 字符串鍵值"foobar"使用 RAW 編碼時的存儲結構

圖4-5 字符串鍵值"123456"的內存結構

redisObject中的refcount字段存儲的是該鍵值被引用數量,即一個鍵值可以被多個鍵引用。Redis啟動后會預先建立10000個分別存儲從0到9999這些數字的redisObject類型變量作為共享對象,如果要設置的字符串鍵值在這10000個數字內(如 SET key1 123)則可以直接引用共享對象而不用再建立一個 redisObject 了,也就是說存儲鍵值占用的空間是0字節,如圖4-6所示。

由此可見,使用字符串類型鍵存儲對象ID這種小數字是非常節省存儲空間的,Redis只需存儲鍵名和一個對共享對象的引用即可。

圖4-6 當執行了 SET key1 123 和 SET key2 123 后,key1 和 key2兩個鍵都直接引用了一個已經建立好的共享對象,節省了存儲空間

提示 當通過配置文件參數 maxmemory 設置了 Redis 可用的最大空間大小時,Redis不會使用共享對象,因為對於每一個鍵值都需要使用一個 redisObject 來記錄其LRU信息。

此外Redis 3.0新加入了 REDIS_ENCODING_EMBSTR 的字符串編碼方式,該編碼方式與REDIS_ENCODING_RAW類似,都是基於sdshdr實現的,只不過sdshdr的結構體與其對應的分配在同一塊連續的內存空間中,如圖4-7所示。

圖4-7 字符串鍵值"foobar"使用 EMBSTR 編碼時的存儲結構

使用REDIS_ENCODING_EMBSTR編碼存儲字符串后,不論是分配內存還是釋放內存,所需要的操作都從兩次減少為一次。而且由於內存連續,操作系統緩存可以更好地發揮作用。當鍵值內容不超過39字節時,Redis 會采用 REDIS_ENCODING_EMBSTR編碼,同時當對使用REDIS_ENCODING_EMBSTR編碼的鍵值進行任何修改操作時(如APPEND命令), Redis會將其轉換成REDIS_ENCODING_RAW編碼。

2.散列類型

散列類型的內部編碼方式可能是 REDIS_ENCODING_HT 或 REDIS_ENCODING_ZIPLIST[9] 。在配置文件中可以定義使用REDIS_ENCODING_ZIPLIST方式編碼散列類型的時機:

hash-max-ziplist-entries 512

hash-max-ziplist-value 64

當散列類型鍵的字段個數少於hash-max-ziplist-entries參數值且每個字段名和字段值的長度都小於 hash-max-ziplist-value 參數值(單位為字節)時,Redis 就會使用 REDIS_ ENCODING_ZIPLIST 來存儲該鍵,否則就會使用 REDIS_ENCODING_HT。轉換過程是透明的,每當鍵值變更后Redis都會自動判斷是否滿足條件來完成轉換。

REDIS_ENCODING_HT編碼即散列表,可以實現O(1)時間復雜度的賦值取值等操作,其字段和字段值都是使用 redisObject 存儲的,所以前面講到的字符串類型鍵值的優化方法同樣適用於散列類型鍵的字段和字段值。

提示 Redis的鍵值對存儲也是通過散列表實現的,與 REDIS_ENCODING_HT 編碼方式類似,但鍵名並非使用 redisObject 存儲,所以鍵名"123456"並不會比"abcdef"占用更少的空間。之所以不對鍵名進行優化是因為絕大多數情況下鍵名都不會是純數字。

補充知識 Redis 支持多數據庫,每個數據庫中的數據都是通過結構體 redisDb 存儲的。redisDb的定義如下:

typedef struct redisDb {

dict *dict;    /* The keyspace for this DB */
dict *expires;   /* Timeout of keys with a timeout set */
dict *blocking_keys;  /* Keys with clients waiting for data (BLPOP) */
dict *ready_keys;  /* Blocked keys that received a PUSH */
dict *watched_keys;  /* WATCHED keys for MULTI/EXEC CAS */
int id;

} redisDb;

dict類型就是散列表結構,expires存儲的是數據的過期時間。當Redis啟動時會根據配置文件中 databases參數指定的數量創建若干個 redisDb類型變量存儲不同數據庫中的數據。

REDIS_ENCODING_ZIPLIST 編碼類型是一種緊湊的編碼格式,它犧牲了部分讀取性能以換取極高的空間利用率,適合在元素較少時使用。該編碼類型同樣還在列表類型和有序集合類型中使用。REDIS_ENCODING_ZIPLIST 編碼結構如圖 4-8 所示,其中 zlbytes是 uint32_t類型,表示整個結構占用的空間。zltail也是 uint32_t類型,表示到最后一個元素的偏移,記錄 zltail 使得程序可以直接定位到尾部元素而無需遍歷整個結構,執行從尾部彈出(對列表類型而言)等操作時速度更快。zllen是uint16_t類型,存儲的是元素的數量。zlend是一個單字節標識,標記結構的末尾,值永遠是255。

圖4-8 REDIS_ENCODING_ZIPLIST編碼的內存結構

在REDIS_ENCODING_ZIPLIST中每個元素由4個部分組成。

第一個部分用來存儲前一個元素的大小以實現倒序查找,當前一個元素的大小小於254字節時第一個部分占用1個字節,否則會占用5個字節。

第二、三個部分分別是元素的編碼類型和元素的大小,當元素的大小小於或等於 63個字節時,元素的編碼類型是ZIP_STR_06B(即0<<6),同時第三個部分用6個二進制位來記錄元素的長度,所以第二、三個部分總占用空間是1字節。當元素的大小大於63且小於或等於16383字節時,第二、三個部分總占用空間是2字節。當元素的大小大於16383字節時,第二、三個部分總占用空間是5字節。

第四個部分是元素的實際內容,如果元素可以轉換成數字的話Redis會使用相應的數字類型來存儲以節省空間,並用第二、三個部分來表示數字的類型(int16_t、int32_t等)。

使用REDIS_ENCODING_ZIPLIST編碼存儲散列類型時元素的排列方式是:元素1存儲字段1,元素2存儲字段值1,依次類推,如圖4-9所示。

例如,當執行命令 HSET hkey foo bar命令后,hkey鍵值的內存結構如圖4-10所示。

圖4-9 使用 REDIS_ENCODING_ZIPLIST編碼存儲散列類型的內存結構

圖4-10 hkey鍵值的內存結構

下次需要執行 HSET hkey foo anothervalue時Redis需要從頭開始找到值為 foo的元素(查找時每次都會跳過一個元素以保證只查找字段名),找到后刪除其下一個元素,並將新值anothervalue插入。刪除和插入都需要移動后面的內存數據,而且查找操作也需要遍歷才能完成,可想而知當散列鍵中數據多時性能將很低,所以不宜將 hash-max-ziplist-entries和hash-max-ziplist-value兩個參數設置得很大。

3.列表類型

列表類型的內部編碼方式可能是 REDIS_ENCODING_LINKEDLIST或 REDIS ENCODING_ZIPLIST。同樣在配置文件中可以定義使用REDIS_ENCODING_ZIPLIST方式編碼的時機:

list-max-ziplist-entries 512

list-max-ziplist-value 64

具體轉換方式和散列類型一樣,這里不再贅述。

REDIS_ENCODING_LINKEDLIST編碼方式即雙向鏈表,鏈表中的每個元素是用redis Object 存儲的,所以此種編碼方式下元素值的優化方法與字符串類型的鍵值相同。

而使用 REDIS_ENCODING_ZIPLIST 編碼方式時具體的表現和散列類型一樣,由於REDIS_ENCODING_ZIPLIST 編碼方式同樣支持倒序訪問,所以采用此種編碼方式時獲取兩端的數據依然較快。

Redis最新的開發版本新增了 REDIS_ENCODING_QUICKLIST編碼方式,該編碼方式是REDIS_ENCODING_LINKEDLIST和REDIS_ENCODING_ZIPLIST的結合,其原理是將一個長列表分成若干個以鏈表形式組織的 ziplist,從而達到減少空間占用的同時提升REDIS_ENCODING_ZIPLIST編碼的性能的效果。

4.集合類型

集合類型的內部編碼方式可能是 REDIS_ENCODING_HT 或 REDIS_ENCODING_INTSET。當集合中的所有元素都是整數且元素的個數小於配置文件中的set-max-intset-entries參數指定值(默認是512)時Redis會使用REDIS_ENCODING_INTSET編碼存儲該集合,否則會使用REDIS_ENCODING_HT來存儲。

REDIS_ENCODING_INTSET編碼存儲結構體intset的定義是:

typedef struct intset {

uint32_t encoding;
uint32_t length;
int8_t contents[];

} intset;

其中contents存儲的就是集合中的元素值,根據encoding的不同,每個元素占用的字節大小不同。默認的encoding是INTSET_ENC_INT16(即2個字節),當新增加的整數元素無法使用 2 個字節表示時,Redis 會將該集合的 encoding 升級為 INTSET_ENC_INT32(即4個字節)並調整之前所有元素的位置和長度,同樣集合的encoding還可升級為INTSET_ENC_INT64(即8個字節)。

REDIS_ENCODING_INTSET編碼以有序的方式存儲元素(所以使用SMEMBERS命令獲得的結果是有序的),使得可以使用二分算法查找元素。然而無論是添加還是刪除元素, Redis 都需要調整后面元素的內存位置,所以當集合中的元素太多時性能較差。

當新增加的元素不是整數或集合中的元素數量超過了set-max-intset-entries參數指定值時,Redis會自動將該集合的存儲結構轉換成REDIS_ENCODING_HT。

注意 當集合的存儲結構轉換成 REDIS_ENCODING_HT 后,即使將集合中的所有非整數元素刪除,Redis也不會自動將存儲結構轉換回 REDIS_ENCODING_INTSET。因為如果要支持自動回轉,就意味着Redis在每次刪除元素時都需要遍歷集合中的鍵來判斷是否可以轉換回原來的編碼,這會使得刪除元素變成了時間復架度為O(n)的操作。

5.有序集合類型

有序集合類型的內部編碼方式可能是 REDIS_ENCODING_SKIPLIST 或 REDIS_ENCODING_ZIPLIST。同樣在配置文件中可以定義使用REDIS_ENCODING_ZIPLIST方式編碼的時機:

zset-max-ziplist-entries 128

zset-max-ziplist-value 64

具體規則和散列類型及列表類型一樣,不再贅述。

當編碼方式是REDIS_ENCODING_SKIPLIST時,Redis使用散列表和跳躍列表(skip list)兩種數據結構來存儲有序集合類型鍵值,其中散列表用來存儲元素值與元素分數的映射關系以實現O(1)時間復雜度的 ZSCORE 等命令。跳躍列表用來存儲元素的分數及其到元素值的映射以實現排序的功能。Redis對跳躍列表的實現進行了幾點修改,其中包括允許跳躍列表中的元素(即分數)相同,還有為跳躍鏈表每個節點增加了指向前一個元素的指針以實現倒序查找。

采用此種編碼方式時,元素值是使用 redisObject 存儲的,所以可以使用字符串類型鍵值的優化方式優化元素值,而元素的分數是使用double類型存儲的。

使用REDIS_ENCODING_ZIPLIST編碼時有序集合存儲的方式按照“元素1的值,元素1的分數,元素2的值,元素2的分數”的順序排列,並且分數是有序的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM