Redis实现之对象(三)
集合對象
集合對象的編碼可以是intset或者hashtable,intset編碼的集合對象使用整數集合作為底層實現,集合對象包含的所有元素都被保存在整數集合里面。舉個栗子,以下代碼將創建一個圖1-12所示的intset編碼集合對象:
127.0.0.1:6379> SADD numbers 1 3 5 (integer) 3 127.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) 3 127.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) 3 127.0.0.1:6379> OBJECT ENCODING numbers "intset" 127.0.0.1:6379> SADD numbers "seven" (integer) 1 127.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) 512 127.0.0.1:6379> OBJECT ENCODING integers "intset" 127.0.0.1:6379> SADD integers 10086 (integer) 1 127.0.0.1:6379> SCARD integers (integer) 513 127.0.0.1:6379> OBJECT ENCODING integers "hashtable"
集合命令的實現
因為集合鍵的值為集合對象,所以用于集合鍵的所有命令都是針對集合對象來操作的,表1-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) 3 127.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) 128 127.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) 1 127.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列出了其中一部分有序集合鍵命令,以及這些命令在不同編碼的哈希對象下的實現方法
| 命令 | ziplist編碼的實現方法 | zset編碼的實現方法 |
| ZADD | 調用ziplistInsert函數, 將成員和分值作為兩個節點分別插入到壓縮列表 | 先調用zslInsert函數,將新元素添加到跳躍表,然后調用dictAdd 函數,將新元素關聯到字典 |
| ZCARD | 調用ziplistLen函數,獲得壓縮列表包含節點的數量,將這個數量除以2得出集合元素的數量 | 訪問跳躍表數據結構的length屬性, 直接返回集合元素的數量 |
| ZCOUNT | 遍歷壓縮列表,統計分值在給定范圍內的節點的數量 | 遍歷跳躍表,統計分值在給定范圍內的節點的數量 |
| ZRANGE | 從表頭向表尾遍歷壓縮列表,返回給定索引范圍內的所有元素 | 從表頭向表尾遍歷跳躍表,返回給定索引范圍內的所有元素 |
| ZREVRANGE | 從表尾向表頭遍歷壓縮列表,返回給定索引范圍內的所有元素 | 從表尾向表頭遍歷跳躍表,返回給定索引范圍內的所有元素 |
| ZRANK | 從表頭向表尾遍歷壓縮列表,查找給定的成員,沿途記錄經過節點的數量,當找到給定成員之后,途經節點的數量就是該成員所對應元素的排名 | 從表頭向表尾遍歷跳躍表,查找給定的成員,沿途記錄經過節點的數量,當找到給定成員之后,途經節點的數量就是該成員所對應元素的排名 |
| ZREVRANK | 從表尾向表頭遍歷壓縮列表,查找給定的成員,沿途記錄經過節點的數量,當找到給定成員之后, 途經節點的數量就是該成員所對應元素的排名 | 從表尾向表頭遍歷跳躍表,查找給定的成員,沿途記錄經過節點的數量,當找到給定成員之后, 途經節點的數量就是該成員所對應元素的排名 |
| ZREM | 遍歷壓縮列表,刪除所有包含給定成員的節點,以及被刪除成員節點旁邊的分值節點 | 遍歷跳躍表,刪除所有包含了給定成員的跳躍表節點。 并在字典中解除被刪除元素的成員和分值的關聯 |
| ZSCORE | 遍歷壓縮列表,查找包含了給定成員的節點,然后取出成員節點旁邊的分值節點保存的元素分值 | 直接從字典中取出給定成員的分值 |
轉載于:https://www.cnblogs.com/beiluowuzheng/p/9737243.html
總結
以上是生活随笔為你收集整理的Redis实现之对象(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2022华为杯数学建模A题思路代码
- 下一篇: 有必要考国二mysql_国二证有用吗