標籤:原因 代碼 back ber rank backward highlight card struct
集合對象
集合對象的編碼可以是intset或者hashtable,intset編碼的集合對象使用整數集合作為底層實現,集合對象包含的所有元素都被儲存在整數集合裡面。舉個栗子,以下代碼將建立一個圖1-12所示的intset編碼集合對象:
127.0.0.1:6379> SADD numbers 1 3 5(integer) 3127.0.0.1:6379> OBJECT ENCODING numbers"intset"
圖1-12 inset編碼的numbers集合對象
另一方面,hashtable編碼的集合對象使用字典作為底層實現,字典的每個鍵都是一個字串對象,每個字串對象包含了一個集合元素,而字典的值則全部被設定為NULL,以下的樣本,將建立一個1-13所示的hashtable編碼集合對象:
127.0.0.1:6379> SADD fruits "apple" "banana" "cherry"(integer) 3127.0.0.1:6379> OBJECT ENCODING fruits"hashtable"
圖1-13 hashtable編碼的fruits集合對象
編碼的轉換
當集合對象可以同時滿足以下兩個條件時,對象使用intset編碼:
- 集合對象儲存的所有元素都是整數值
- 集合對象儲存的元素數量不超過512個
不能滿足以上兩個條件對的集合對象需要使用hashtable編碼,注意,第一個條件是無法修改的,但第二個條件的上限值可以修改,具體請看設定檔中關於set-max-intset-entries選項的說明
對於使用intset編碼的集合對象來說,當使用intset編碼所需的兩個條件的任意一個不能被滿足時,就會執行對象的編碼轉換操作,原本儲存在整數集合中的所有元素都會被轉移並儲存到字典裡面,並且對象的編碼也會從intset變為hashtable
舉個栗子,以下代碼建立一個只包含整數元素的集合對象,該對象原來的編碼為intset,但我們只要添加一個字串元素,集合對象的編碼轉移操作就會被執行
127.0.0.1:6379> SADD numbers 1 3 5(integer) 3127.0.0.1:6379> OBJECT ENCODING numbers"intset"127.0.0.1:6379> SADD numbers "seven"(integer) 1127.0.0.1:6379> OBJECT ENCODING numbers"hashtable"
除此之外,如果我們建立一個包含512個整數元素的集合對象,那麼對象的編碼應該是intset。但是,只要我們再往集合添加一個整數元素,使得這個集合的元素變為513,那麼對象的編碼轉換操作就會被執行:
127.0.0.1:6379> EVAL "for i=1, 512 do redis.call(‘SADD‘, KEYS[1], i) end" 1 integers(nil)127.0.0.1:6379> SCARD integers(integer) 512127.0.0.1:6379> OBJECT ENCODING integers"intset"127.0.0.1:6379> SADD integers 10086(integer) 1127.0.0.1:6379> SCARD integers(integer) 513127.0.0.1:6379> OBJECT ENCODING integers"hashtable"
集合命令的實現
因為集合鍵的值為集合對象,所以用於集合鍵的所有命令都是針對集合對象來操作的,表1-10列出了其中一部分集合鍵的命令,以及這些命令在不同編碼的集合對象下的實現方法
表8-10集合命令的實現方法
| 命令 |
intset編碼的實現方法 |
hashtable編碼的實現方法 |
| SADD |
調用intsetAdd函數,將所有新元素添加到整數集合裡面 |
調用dictAdd,以新元素為鍵,NULL為值,將索引值對添加到字典裡面 |
| SCARD |
調用intsetLen函數,返回整數集合所包含的元素數量,這個數量就是集合對象所包含的元素數量 |
調用dictSize函數,返回字典所包含的索引值對數量,這個數量就是集合對象所包含的元素數量 |
| SISMEMBER |
調用intsetFind函數,在整數集合中尋找給定的元素,如果找到了說明元素存在於集合,沒找到則說明元素不存在於集合 |
調用dictFind 函數,在字典的鍵中尋找給定的元素,如果找到了說明元素存在於集合,沒找到則說明元素不存在於集合 |
| SMEMBERS |
遍曆整個整數集合,使用intsetGet函數返回集合元素 |
遍曆整個字典,使用dictGetKey函數返回字典的鍵作為集合元素 |
| SRANDMEMBER |
調用intsetRandom函數,從整數集合中隨機返回一個元素 |
調用dictGetRandomKey函數,從字典中隨機返回一個字典鍵 |
| SPOP |
調用intsetRandom函數,從整數集合中隨機取出一個元素,在將這個隨機元素返回給用戶端之後,調用intsetRemove函數, 將隨機元素從整數集合中刪除掉 |
調用dictGetRandomKey函數,從字典中隨機取出一個字典鍵,在將這個隨機字典鍵的值返回給用戶端之後,調用 dictDelete函數,從字典中刪除隨機字典鍵所對應的索引值對 |
| SREM |
調用intsetRemove函數,從整數集合中刪除所有給定的元素 |
調用dictDelete函數,從字典中刪除所有鍵為給定元素的索引值對 |
有序集合對象
有序集合的編碼可以是ziplist或者skiplist,ziplist編碼的壓縮列表中,每個集合元素使用兩個緊挨在一起的壓縮列表節點來儲存,第一個節點儲存元素的成員(member),而第二個元素儲存元素的分值(score)。壓縮列表內的集合元素按分值從小到大進行排序,分值較小的元素被放置在靠近表頭的方向,而分值較大的元素則被放置在靠近表尾的方向
舉個栗子,如果我們執行以下ZADD命令,那麼伺服器將建立一個有序集合對象作為price鍵的值:
127.0.0.1:6379> ZADD price 8.5 apple 5.0 banana 6.0 cherry(integer) 3127.0.0.1:6379> OBJECT ENCODING price"ziplist"
price這個值對象1-14所示,而對象所使用的的壓縮列表1-15所示
圖1-14 ziplist編碼的有序集合對象
圖1-15 有序集合元素在壓縮列表中按分值從小到大排列
skiplist編碼的有序集合對象使用zset結構作為底層實現,一個zset結構同時包含一個字典和一個跳躍表
redis.h
typedef struct zset { dict *dict; zskiplist *zsl;} zset;typedef struct zskiplistNode { robj *obj; double score; struct zskiplistNode *backward; struct zskiplistLevel { struct zskiplistNode *forward; unsigned int span; } level[];} zskiplistNode;typedef struct zskiplist { struct zskiplistNode *header, *tail; unsigned long length; int level;} zskiplist;
zset結構中的的zsl跳躍表按分值從小到大儲存了所有集合元素,每個跳躍表節點都儲存了一個集合元素:跳躍表節點的obj屬性儲存了元素的成員,而跳躍表節點的score屬性則儲存了元素的分值。通過這個跳躍表,程式可以對有序集合進行範圍型操作,比如ZRANK、ZRANGE等命令就是基於跳躍表API來實現的
除此之外,zset結構中的dict字典為有序集合建立了一個從成員到分值的映射,字典中的每個索引值對都儲存了一個集合元素:字典的鍵儲存了元素的成員,而字典的值則儲存了元素的分值。通過這個字典,程式可以在O(1)的時間複雜度內尋找給定成員的分值,ZSCORE命令就是根據這一特性實現的,而很多其他有序集合命令都在實現的內部用到了這一特性
有序集合中每個元素的成員都是一個字串對象,而每個元素的分值都是一個double類型的浮點數。值得一提的是,雖然zset結構同時使用跳躍表和字典來儲存有序集合元素,但這兩種資料結構都會通過指標來共用相同元素的成員和分值,所以同時使用跳躍表和字典來儲存集合元素不會產生任何重複成員或分值,也不會因為浪費額外的記憶體
為什麼有序集合需要同時使用跳躍表和字典來實現?在理論上,有序集合可以單獨使用字典或者跳躍表其中一種資料結構來實現,但無論使用字典還是跳躍表,在效能上比起同時使用字典和跳躍表都會有所降低。舉個例子,如果我們只是用字典來實現有序集合,那麼雖然可以在O(1)的時間複雜度內尋找成員對應的分值,但是,因為字典以無序的方式來儲存集合元素,所以每次在執行範圍型操作——比如:ZRANK、ZRANGE等命令時,程式都需要對字典的所有元素進行排序,完成這種排序至少需要O(N logN)的時間複雜度,以及額外的O(N)記憶體空間(因為要建立一個數組來儲存排序後的元素)
另一方面如果我們只使用跳躍表來實現有序集合,那麼跳躍表執行範圍型操作時的所有優點都會被保留,但因為沒有了字典,所以根據成員尋找分值這一操作的時間複雜度將從O(1)上升至O(logN)。因為以上原因,為了讓有序集合的尋找和範圍型操作都儘可能快地執行,Redis選擇了同時使用字典和跳躍表兩種資料結構來實現有序集合
舉個栗子,如果前面的price鍵建立的不是ziplist編碼的有序集合對象,而是skiplist編碼的有序集合對象,那麼這個有序集合對象將會是圖1-16所示的樣子,而對象所使用的zset結構將會是圖8-17所示的樣子
圖1-16 skiplist編碼的有序集合對象
圖1-17 有序集合元素同時被儲存在字典和跳躍表中
編碼的轉換
當有序集合對象可以同時滿足以下條件時,對象使用ziplist編碼:
- 有序集合儲存的元素數量小於128個
- 有序集合儲存的所有元素的長度小於64位元組
不能滿足以上兩個條件的有序集合對象將使用skiplist編碼
# 對象包含了 128 個元素127.0.0.1:6379> EVAL "for i=1, 128 do redis.call(‘ZADD‘, KEYS[1], i, i) end" 1 numbers(nil)127.0.0.1:6379> ZCARD numbers(integer) 128127.0.0.1:6379> OBJECT ENCODING numbers"ziplist"# 再添加一個新元素127.0.0.1:6379> ZADD numbers 3.14 pi(integer) 1# 對象包含的元素數量變為 129 個127.0.0.1:6379> ZCARD numbers(integer) 129# 編碼已改變127.0.0.1:6379> OBJECT ENCODING numbers"skiplist"
以下代碼則展示了有序集合對象因為元素的成員過長而引發編碼轉換的情況:
# 向有序集合添加一個成員只有三位元組長的元素127.0.0.1:6379> ZADD blah 1.0 www(integer) 1127.0.0.1:6379> OBJECT ENCODING blah"ziplist"# 向有序集合添加一個成員為 66 位元組長的元素127.0.0.1:6379> ZADD blah 2.0 oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo(integer) 1# 編碼已改變127.0.0.1:6379> OBJECT ENCODING blah"skiplist"
有序集合命令的實現
因為有序集合鍵的值為雜湊值,所以用於有序集合鍵的所有命令都是針對雜湊對象來構建的,表1-11列出了其中一部分有序集合鍵命令,以及這些命令在不同編碼的雜湊對象下的實現方法
表 8-11有序集合命令的實現方法
| 命令 |
ziplist編碼的實現方法 |
zset編碼的實現方法 |
| ZADD |
調用ziplistInsert函數, 將成員和分值作為兩個節點分別插入到壓縮列表 |
先調用zslInsert函數,將新元素添加到跳躍表,然後調用dictAdd 函數,將新元素關聯到字典 |
| ZCARD |
調用ziplistLen函數,獲得壓縮列表包含節點的數量,將這個數量除以2得出集合元素的數量 |
訪問跳躍表資料結構的length屬性, 直接返回集合元素的數量 |
| ZCOUNT |
遍曆壓縮列表,統計分值在給定範圍內的節點的數量 |
遍曆跳躍表,統計分值在給定範圍內的節點的數量 |
| ZRANGE |
從表頭向表尾遍曆壓縮列表,返回給定索引範圍內的所有元素 |
從表頭向表尾遍曆跳躍表,返回給定索引範圍內的所有元素 |
| ZREVRANGE |
從表尾向表頭遍曆壓縮列表,返回給定索引範圍內的所有元素 |
從表尾向表頭遍曆跳躍表,返回給定索引範圍內的所有元素 |
| ZRANK |
從表頭向表尾遍曆壓縮列表,尋找給定的成員,沿途記錄經過節點的數量,當找到給定成員之後,途經節點的數量就是該成員所對應元素的排名 |
從表頭向表尾遍曆跳躍表,尋找給定的成員,沿途記錄經過節點的數量,當找到給定成員之後,途經節點的數量就是該成員所對應元素的排名 |
| ZREVRANK |
從表尾向表頭遍曆壓縮列表,尋找給定的成員,沿途記錄經過節點的數量,當找到給定成員之後, 途經節點的數量就是該成員所對應元素的排名 |
從表尾向表頭遍曆跳躍表,尋找給定的成員,沿途記錄經過節點的數量,當找到給定成員之後, 途經節點的數量就是該成員所對應元素的排名 |
| ZREM |
遍曆壓縮列表,刪除所有包含給定成員的節點,以及被刪除成員節點旁邊的分值節點 |
遍曆跳躍表,刪除所有包含了給定成員的跳躍表節點。 並在字典中解除被刪除元素的成員和分值的關聯 |
| ZSCORE |
遍曆壓縮列表,尋找包含了給定成員的節點,然後取出成員節點旁邊的分值節點儲存的元素分值 |
直接從字典中取出給定成員的分值 |
Redis實現之對象(三)