Redis的五種數據類型、方法、底層數據結構

1、字符串string

2、列表list

3、散列hash

4、集合set

5、有序集合sorted set

 

字符串string

字符串類型是Redis中最爲基礎的數據存儲類型,是一個由字節組成的序列,他在Redis中是二進制安全的,這便意味着該類型可以接受任何格式的數據,如JPEG圖像數據貨Json對象描述信息等,是標準的key-value,一般來存字符串,整數和浮點數。Value最多可以容納的數據長度爲512MB
應用場景:很常見的場景用於統計網站訪問數量,當前在線人數等。incr命令(++操作)

 

列表list

Redis的列表允許用戶從序列的兩端推入或者彈出元素,列表由多個字符串值組成的有序可重複的序列,是鏈表結構,所以向列表兩端添加元素的時間複雜度爲0(1),獲取越接近兩端的元素速度就越快。這意味着即使是一個有幾千萬個元素的列表,獲取頭部或尾部的10條記錄也是極快的。List中可以包含的最大元素數量是4294967295。
應用場景:1.最新消息排行榜。2.消息隊列,以完成多程序之間的消息交換。可以用push操作將任務存在list中(生產者),然後線程在用pop操作將任務取出進行執行。(消費者)

 

散列hash

Redis中的散列可以看成具有String key和String value的map容器,可以將多個key-value存儲到一個key中。每一個Hash可以存儲4294967295個鍵值對。
應用場景:例如存儲、讀取、修改用戶屬性(name,age,pwd等)

 

集合set

Redis的集合是無序不可重複的,和列表一樣,在執行插入和刪除和判斷是否存在某元素時,效率是很高的。集合最大的優勢在於可以進行交集並集差集操作。Set可包含的最大元素數量是4294967295。
應用場景:1.利用交集求共同好友。2.利用唯一性,可以統計訪問網站的所有獨立IP。3.好友推薦的時候根據tag求交集,大於某個threshold(臨界值的)就可以推薦。

 

有序集合sorted set

和set很像,都是字符串的集合,都不允許重複的成員出現在一個set中。他們之間差別在於有序集合中每一個成員都會有一個分數(score)與之關聯,Redis正是通過分數來爲集合中的成員進行從小到大的排序。儘管有序集合中的成員必須是唯一的,但是分數(score)卻可以重複。
應用場景:可以用於一個大型在線遊戲的積分排行榜,每當玩家的分數發生變化時,可以執行zadd更新玩家分數(score),此後在通過zrange獲取幾分top ten的用戶信息。

 

最後,還有個對key的通用操作,所有的數據類型都可以使用的

 

=========

zset

zset底層存儲結構

 zset底層的存儲結構包括ziplist或skiplist,在同時滿足以下兩個條件的時候使用ziplist,其他時候使用skiplist,兩個條件如下:

  • 有序集合保存的元素數量小於128個
  • 有序集合保存的所有元素的長度小於64字節

redis配置文件中用來控制zset到底是使用ziplist(壓縮雙向鏈表)還是skiplist(跳錶)的參數:

zset-max-ziplist-entries 128

zset-max-ziplist-value 64

zset-max-ziplist-entries zset使用ziplist存儲的時候,最大限制存儲entries的個數
zset-max-ziplist-value zset使用ziplist存儲的時候,每個節點最大存儲字節數

違反上述兩個限制條件,均會導致zset將ziplist的數據結構切換爲skiplist數據結構

而zset使用ziplist的原因,主要是出於在零散數據量少的時候,節省內容的佔用

當ziplist作爲zset的底層存儲結構時候,每個集合元素使用兩個緊挨在一起的壓縮列表節點來保存,第一個節點保存元素的成員,第二個元素保存元素的分值。

 當skiplist作爲zset的底層存儲結構的時候,使用skiplist按序保存元素及分值,使用dict來保存元素和分值的映射關係


 

操作單個zset的redis命令

1、添加,如果值存在添加,將會重新排序。zadd

127.0.0.1:6379>zadd myZSet 1 zlh   ---添加分數爲1,值爲zlh的zset集合

127.0.0.1:6379>zadd mySet 2 Tom 3 Jim   ---添加2條zset集合,分別爲分數爲2、3,值爲tom、jim的集合。

2、查看zset集合的成員個數。zcard

127.0.0.1:6379>zcard myZSet   ---輸出zset的成員個數爲3

3、查看Zset指定範圍的成員,withscores爲輸出結果帶分數。zrange

127.0.0.1:6379>zrange mZySet 0 -1   ----0爲開始,-1爲結束,輸出順序結果爲: zlh tom jim

127.0.0.1:6379>zrange mZySet 0 -1 withscores   ---輸出帶分數的結果爲:zlh 1 tom 2 jim 3

4、獲取zset成員的下標位置,如果值不存在返回null。zrank

127.0.0.1:6379>zrank mZySet Jim    ---Jim的在zset集合中的下標爲2

5、獲取zset集合指定分數之間存在的成員個數。zcount

127.0.0.1:6379>zcount mySet 1 3   ---輸出分數>=1 and 分數 <=3的成員個數爲3,因爲分數是可以重複的,所以這個命令是有道理的。

6、刪除指定的一個成員或多個成員。zrem

127.0.0.1:6379>zrem myZSet zlh   --刪除值爲zlh的zset成員

127.0.0.1:6379>zrem myZSet Tom Jim    ---刪除值爲Tom和Jim的兩個zset成員

7、獲取指定值的分數。zscore

127.0.0.1:6379>zadd myZset 1 zlh 1 tom 2 jim 3 xdd 4 pmm   ---由於上面的數據被刪除完了,這裏添加5條示範數據再。

127.0.0.1:6379>zscore myZset zlh    ---輸出值爲zlh的分數爲1

8、給指定元素的分數進行增減操作,負值爲減,正值爲加。zincrby

127.0.0.1:6379>zscore myZset tom    ----輸出tom的分數爲1

127.0.0.1:6379>zincrby myZset 4 tom   ---tom的分數值加4,輸入分數值爲5

127.0.0.1:6379>zscore myZset tom    ---輸出tom的分數值爲5

9、根據指定分數的範圍獲取值。zrangebysocre

127.0.0.1:6379>zrangebyscore myZset  1 5   ---輸出分數>=1 and <=5的成員值爲:zlh jim xdd pmm  tom

127.0.0.1:6379>zrangebyscore myZset  (1 5    ----輸出分數>1 and <=5的成員值爲:jim xdd pmm tom

127.0.0.1:6379>zrangebyscore myZset 2 5 limit 1 2    ---檢索分數爲2到5之間的數據,然後從下標爲1的數據開始往後輸出2個數據,包含下標爲1的數據。結果爲:xdd pmm

127.0.0.1:6379>zrangebyscore myZset -inf +inf limit 2 3   ----+inf表示最後一個成員,-inf表示第一個成員,意思是:檢索所有數據,然後從下標爲2的數據開始再往後輸出2個數據。結果爲:xdd pmm tom

10、倒序,從高到底排序輸出指定範圍的數據。zrevrange,zrevrangebyscore

127.0.0.1:6379>zrevrange myZset 2 3   ---先倒序排列數據,輸出下標爲>=2 and <=3的數據爲xdd jim,這裏注意的是倒序之後下標也反過來了。

127.0.0.1:6379>zrevrange myZset 2 4 withscores    ---輸出結果爲:xdd 3 jim 2 zlh 1

127.0.0.1:6379>zrevrangebyscore myZset 5 1 limit  3 2  ----輸出結果爲:jim zlh 。獲取score <=5 and >=1,從下標爲爲3開始獲取2條數據。

127.0.0.1:6379>zrevrangebyscore myZset 4 2   ----分數>=2 and <=4 的數據倒序輸出:pmm xdd jim

11、根據座標,分數範圍刪除數據。zremrangebyscore,zremrangebyrank

127.0.0.1:6379>zremrangebyscore myZset 1 2   ---刪除分數>=1 and <=2的數據

127.0.0.1:6379>zrange myZset 0 -1    ----輸出結果爲 xdd pmm tom

127.0.0.1:6379>zremrangebyrank myZset 0 2    ---刪除下標>=0 and <=2的zset元素

127.0.0.1:6379>zrange myZset 0 -1    --輸出結果爲:empty list or set 。沒數據啦。

操作多個zset的redis命令

1、求多個zset的並集

127.0.0.1:6379>zadd myZset 1 zlh 2 jim 3 tom   ---添加3個數據

127.0.0.1:6379>zadd youZset 1 zlh 2 xdd 3 pmm    ---添加3個數據

127.0.0.1:6379>zunionzstore heZset 2 myZset youZset  ----將myzset和youzset的並集添加到hezset中。

2、求多個zset的交集

127.0.0.1:6379>zinterstore sheZset 2 myZset youZset  ----將myzset和youZset 的交集添加到sheZset中。

 

 

===圖解redis五種數據結構底層實現===========

redis有五種基本數據結構:字符串、hash、set、zset、list。但是你知道構成這五種結構的底層數據結構是怎樣的嗎? 今天我們來花費五分鐘的時間瞭解一下。 (目前redis版本爲3.0.6)

動態字符串SDS

SDS是"simple dynamic string"的縮寫。 redis中所有場景中出現的字符串,基本都是由SDS來實現的

所有非數字的key。例如 set msg"hello world" 中的key msg.字符串數據類型的值。例如`` set msg "hello world"中的msg的值"hello wolrd"非字符串數據類型中的“字符串值”。例如 RPUSH fruits"apple""banana""cherry"中的"apple" "banana" "cherry"SDS長這樣:

 

free:還剩多少空間 len:字符串長度 buf:存放的字符數組

空間預分配

爲減少修改字符串帶來的內存重分配次數,sds採用了“一次管夠”的策略:

若修改之後sds長度小於1MB,則多分配現有len長度的空間若修改之後sds長度大於等於1MB,則擴充除了滿足修改之後的長度外,額外多1MB空間

惰性空間釋放

爲避免縮短字符串時候的內存重分配操作,sds在數據減少時,並不立刻釋放空間。

int

就是redis中存放的各種數字 包括一下這種,故意加引號“”的

 

雙向鏈表

長這樣:

 

分兩部分,一部分是“統籌部分”:橘黃色,一部分是“具體實施方“:藍色。

主體”統籌部分“:

head指向具體雙向鏈表的頭tail指向具體雙向鏈表的尾len雙向鏈表的長度具體"實施方":一目瞭然的雙向鏈表結構,有前驅 pre有後繼 next

由 list和 listNode兩個數據結構構成。

ziplist

壓縮列表。 redis的列表鍵和哈希鍵的底層實現之一。此數據結構是爲了節約內存而開發的。和各種語言的數組類似,它是由連續的內存塊組成的,這樣一來,由於內存是連續的,就減少了很多內存碎片和指針的內存佔用,進而節約了內存。

然後文中的 entry的結構是這樣的:

 

元素的遍歷

先找到列表尾部元素:

然後再根據ziplist節點元素中的 previous_entry_length屬性,來逐個遍歷:

連鎖更新

再次看看 entry元素的結構,有一個 previous_entry_length字段,他的長度要麼都是1個字節,要麼都是5個字節:

前一節點的長度小於254字節,則 previous_entry_length長度爲1字節前一節點的長度小於254字節,則 previous_entry_length長度爲5字節假設現在存在一組壓縮列表,長度都在250字節至253字節之間,突然新增一新節點 new, 長度大於等於254字節,會出現:

程序需要不斷的對壓縮列表進行空間重分配工作,直到結束。

除了增加操作,刪除操作也有可能帶來“連鎖更新”。 請看下圖,ziplist中所有entry節點的長度都在250字節至253字節之間,big節點長度大於254字節,small節點小於254字節。

哈希表

哈希表略微有點複雜。哈希表的製作方法一般有兩種,一種是: 開放尋址法,一種是 拉鍊法。redis的哈希表的製作使用的是 拉鍊法。

整體結構如下圖:

 

也是分爲兩部分:左邊橘黃色部分和右邊藍色部分,同樣,也是”統籌“和”實施“的關係。 具體哈希表的實現,都是在藍色部分實現的。 先來看看藍色部分:

 

這也分爲左右兩邊“統籌”和“實施”的兩部分。

右邊部分很容易理解:就是通常拉鍊表實現的哈希表的樣式;數組就是bucket,一般不同的key首先會定位到不同的bucket,若key重複,就用鏈表把衝突的key串起來。

新建key的過程:

假如重複了:

rehash

再來看看哈希表總體圖中左邊橘黃色的“統籌”部分,其中有兩個關鍵的屬性: ht和 rehashidx。 ht是一個數組,有且只有倆元素ht[0]和ht[1];其中,ht[0]存放的是redis中使用的哈希表,而ht[1]和rehashidx和哈希表的 rehash有關。

rehash指的是重新計算鍵的哈希值和索引值,然後將鍵值對重排的過程。

加載因子(load factor)=ht[0].used/ht[0].size。

擴容和收縮標準

擴容:

沒有執行BGSAVE和BGREWRITEAOF指令的情況下,哈希表的 加載因子大於等於1。正在執行BGSAVE和BGREWRITEAOF指令的情況下,哈希表的 加載因子大於等於5。收縮:

加載因子小於0.1時,程序自動開始對哈希表進行收縮操作。擴容和收縮的數量

擴容:

第一個大於等於 ht[0].used*2的 2^n(2的n次方冪)。收縮:

第一個大於等於 ht[0].used的 2^n(2的n次方冪)。(以下部分屬於細節分析,可以跳過直接看擴容步驟)

對於收縮,我當時陷入了疑慮:收縮標準是 加載因子小於0.1的時候,也就是說假如哈希表中有4個元素的話,哈希表的長度只要大於40,就會進行收縮,假如有一個長度大於40,但是存在的元素爲4即( ht[0].used爲4)的哈希表,進行收縮,那收縮後的值爲多少?

我想了一下:按照前文所講的內容,應該是4。 但是,假如是4,存在和收縮後的長度相等,是不是又該擴容? 翻開源碼看看:

收縮具體函數:

int dictResize(dict *d) { int minimal; //如果dict_can_resize被設置成0,表示不能進行rehash,或正在進行rehash,返回出錯標誌DICT_ERR if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR; minimal = d->ht[0].used; //獲得已經有的節點數量作爲最小限度minimal if (minimal < DICT_HT_INITIAL_SIZE)//但是minimal不能小於最低值DICT_HT_INITIAL_SIZE(4) minimal = DICT_HT_INITIAL_SIZE; return dictExpand(d, minimal); //用minimal調整字典d的大小} int dictExpand(dict *d, unsigned long size) { dictht n; unsigned long realsize = _dictNextPower(size); //獲得一個最接近2^n的realsize if (dictIsRehashing(d) || d->ht[0].used > size) //正在rehash或size不夠大返回出錯標誌 return DICT_ERR; if (realsize == d->ht[0].size) return DICT_ERR; //如果新的realsize和原本的size一樣則返回出錯標誌 /* Allocate the new hash table and initialize all pointers to NULL */ //初始化新的哈希表的成員 n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; /* Is this the first initialization? If so it's not really a rehashing * we just set the first hash table so that it can accept keys. */ if (d->ht[0].table == NULL) { //如果ht[0]哈希表爲空,則將新的哈希表n設置爲ht[0] d->ht[0] = n; return DICT_OK; } d->ht[1] = n; //如果ht[0]非空,則需要rehash d->rehashidx = 0; //設置rehash標誌位爲0,開始漸進式rehash(incremental rehashing) return DICT_OK;} static unsigned long _dictNextPower(unsigned long size){ unsigned long i = DICT_HT_INITIAL_SIZE; //DICT_HT_INITIAL_SIZE 爲 4 if (size >= LONG_MAX) return LONG_MAX + 1LU; while(1) { if (i >= size) return i; i *= 2; }}由代碼我們可以看到,假如收縮後長度爲4,不僅不會收縮,甚至還會報錯。()

我們回過頭來再看看設定:題目可能成立嗎? 哈希表的擴容都是2倍增長的,最小是4, 4 ===》 8 ====》 16 =====》 32 ======》 64 ====》 128

也就是說:不存在長度爲 40多的情況,只能是64。但是如果是64的話,64 X 0.1(收縮界限)= 6.4 ,也就是說在減少到6的時候,哈希表就會收縮,會縮小到多少呢?是8。此時,再繼續減少到4,也不會再收縮了。所以,根本不存在一個長度大於40,但是存在的元素爲4的哈希表的。

擴容步驟

收縮步驟

漸進式refresh

在"擴容步驟"和"收縮步驟" 兩幅動圖中每幅圖的第四步驟“將ht[0]中的數據利用哈希函數重新計算,rehash到ht[1]”,並不是一步完成的,而是分成N多步,循序漸進的完成的。 因爲hash中有可能存放幾千萬甚至上億個key,畢竟Redis中每個hash中可以存 2^32-1 鍵值對(40多億),假如一次性將這些鍵值rehash的話,可能會導致服務器在一段時間內停止服務,畢竟哈希函數就得計算一陣子呢((#^.^#))。

哈希表的refresh是分多次、漸進式進行的。

漸進式refresh和下圖中左邊橘黃色的“統籌”部分中的 rehashidx密切相關:

rehashidx 的數值就是現在rehash的元素位置rehashidx 等於 -1 的時候說明沒有在進行refresh

 

 

甚至在進行期間,每次對哈希表的增刪改查操作,除了正常執行之外,還會順帶將ht[0]哈希表相關鍵值對rehash到ht[1]。

以擴容步驟爲例:

intset

整數集合是集合鍵的底層實現方式之一。

 

跳錶

跳錶這種數據結構長這樣:

 

redis中把跳錶抽象成如下所示:

 

看這個圖,左邊“統籌”,右邊實現。 統籌部分有以下幾點說明:

header: 跳錶表頭tail:跳錶表尾level:層數最大的那個節點的層數length:跳錶的長度實現部分有以下幾點說明:

表頭:是鏈表的哨兵節點,不記錄主體數據。是個雙向鏈表分值是有順序的o1、o2、o3是節點所保存的成員,是一個指針,可以指向一個SDS值。層級高度最高是32。沒每次創建一個新的節點的時候,程序都會隨機生成一個介於1和32之間的值作爲level數組的大小,這個大小就是“高度”redis五種數據結構的實現

redis對象

redis中並沒有直接使用以上所說的各種數據結構來實現鍵值數據庫,而是基於一種對象,對象底層再間接的引用上文所說的具體的數據結構。

結構如下圖:

 

字符串

 

其中:embstr和raw都是由SDS動態字符串構成的。唯一區別是:raw是分配內存的時候,redisobject和 sds 各分配一塊內存,而embstr是redisobject和raw在一塊兒內存中。

列表

 

hash

 

set

 

zset

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章