天天看點

面試中經常問到的Redis七種資料類型,你都真正了解嗎?

Redis提供更加豐富的資料結構,如:字元串、清單、集合、有序集合、哈希、位圖、HyperLogLogs,你都真正了解嘛?

前言

Redis不是一個簡單的鍵值對存儲,它實際上是一個支援各種類型資料結構的存儲。在傳統的鍵值存儲中,是将字元串鍵關聯到字元串值,但是在Redis中,這些值不僅限于簡單的字元串,還可以支援更複雜的資料結構。下面就是Redis支援的資料結構:

  • 字元串(String):二進制安全字元串。
  • 清單(List):根據插入順序排序的字元串元素清單,基于連結清單實作。
  • 集合(Set):唯一的亂序的字元串元素的集合。
  • 有序集合(Sorted Set):與集合類似,但是每個字元串元素都與一個稱為score的數字相關聯。 元素總是按其score排序,并且可以檢索一定score範圍的元素。
  • 哈希(Hash):由字段與值相關聯組成的映射,字段和值都是字元串。
  • 位圖(Bitmap):像操作位數組一樣操作字元串值,可以設定和清除某個位,對所有為1的位進行計數,找到第一個設定1的位,找到第一個設定0的位等等。
  • HyperLogLogs:一種機率資料結構,使用較小的記憶體空間來統計唯一進制素的數量,誤差小于1%。

歡迎關注微信公衆号:

萬貓學社

,每周一分享Java技術幹貨。

鍵(Key)

是二進制安全的,這意味着您可以使用任何二進制序列作為鍵,可以是

OneMoreStudy

這樣的字元串,也可以使圖檔檔案的内容,空字元串也是有效的

。不過,還有一些其他規則:

  • 不要使用過長的

    ,比如一個1KB的鍵。不僅是多占記憶體方面的問題,而是在資料集中查找

    可能需要進行一些耗時的

    比較。如果真的有比較大的

    ,先對它進行哈希(比如:

    MD5

    SHA1

    )是一個好主意。
  • 也不要使用過短的

    ,比如:

    OMS100f

    ,相對于

    one-more-study:100:fans

    ,後者更具有可讀性。可能會占用更多記憶體,但是相對于值所占的記憶體,

    所增加的記憶體還是小很多的。我們要找到一個平衡點,不長也不短。
  • 多個字段以冒号分隔,一個字段内多個單詞以連詞符或點分隔,比如:

    one-more-study:100:fans

    ,或者

    one.more.study:100:fans

  • 允許的最大值為512MB。

字元串(String)

字元串類型是和

關聯的最簡單的類型。它是Memcached中唯一的資料類型,是以對于新手來說,在Redis中使用它也是很容易的。

是字元串類型,當我們也使用字元串類型作為值時,我們會可以從一個字元串映射到另一個字元串。 字元串資料類型有很多應用場景,例如緩存HTML片段或頁面。

下面簡單介紹一下字元串的指令(在redis-cli中使用):

> set one-more-key OneMoreStudy
OK
> get one-more-key
"OneMoreStudy"
           

使用

SET

GET

指令來設定和查詢字元串值的方式。需要注意的是,如果目前

已經和字元串值相關聯,

SET

指令将會替換已存儲在

中的現有值。字元串可以是任意的二進制資料,比如jpeg圖像。字元串最多不能大于512MB。

SET

指令還有一些實用的可選參數,比如:

> set one-more-key Java nx   #如果key存在,則設定失敗。
(nil)
> set one-more-key Java xx   #如果key存在,才設定成功。
OK
           

雖然字元串是Redis的基本值,但也可以使用它們執行一些實用的操作。 比如:

> set one-more-counter 50
OK
> incr one-more-counter   #自增加1
(integer) 51
> incr one-more-counter   #自增加1
(integer) 52
> incrby one-more-counter 5   #自增加5
(integer) 57
           

INCR

指令将字元串值解析為整數,将其自增加1,最後将獲得的值設定為新值。 還有其他類似的指令,例如

INCRBY

DECR

DECRBY

等指令。

INCR

指令是原子操作,即時有多個用戶端同時同一個key的

INCR

指令,也不會進入競态條件。比如,上面的例子先設定

one-more-counter

的值為50,即使兩個用戶端同時發出INCR指令,那麼最後的值也肯定是52。

可以使用

MSET

MGET

指令在單個指令中設定或查詢多個

的值,對于減少延遲也很有用。比如:

> mset a 1 b 2 c 3
OK
> mget a b c
1) "1"
2) "2"
3) "3"
           

MGET

指令時,Redis傳回一個值的數組。

使用DEL指令可以删除

和相關聯的值,存在指定的

則傳回1,不存在指定的

則傳回0。使用

EXISTS

指令判斷Redis中是否存在指定的

,存在指定的

則傳回0。比如:

> set one-more-key OneMoreStudy
OK
> exists one-more-key
(integer) 1
> del one-more-key
(integer) 1
> exists one-more-key
(integer) 0
           

TYPE

指令,可以傳回存儲在指定key的值的資料類型,比如:

> set one-more-key OneMoreStudy
OK
> type one-more-key
string
> del one-more-key
(integer) 1
> type one-more-key
none
           

在讨論更複雜的資料結構之前,我們需要讨論另一個功能,該功能無論值類型是什麼都适用,它就是

EXPIRE

指令。 它可以為

設定到期時間,當超過這個到期時間後,該

将自動銷毀,就像對這個

調用了

DEL

指令一樣。比如:

> set one-more-key OneMoreStudy
OK
> expire one-more-key 5
(integer) 1
> get one-more-key #立刻調用
"OneMoreStudy"
> get one-more-key #5秒鐘後調用
(nil)
           

上面的例子,适用了

EXPIRE

指令設定了過期時間,也可以使用

PERSIST

指令移除

的過期時間,這個

将持久保持。除了

EXPIRE

指令,還可以使用SET指令設定過期時間,比如:

> set one-more-key OneMoreStudy ex 10 #設定過期時間為10秒
OK
> ttl one-more-key
(integer) 9
           

上面的例子,設定了一個字元串值

OneMoreStudy

one-more-key

,該

的到期時間為10秒。之後,調用

TTL

指令以檢查該

的剩餘生存時間。

到期時間可以使用秒或毫秒精度進行設定,但到期時間的分辨率始終為1毫秒。實際上,Redis伺服器上存儲的不是到期時間長度,而是該

到期的時間。

清單(List)

Redis清單是使用連結清單實作的,這就意味着在頭部或尾部增加或删除一個的元素的時間複雜度是O(1),非常快的。不過,按索引查詢對應元素的時間複雜度就是O(n),慢很多。如果想快速查詢大量資料,可以使用有序集合,後面會有介紹。

LPUSH

指令将一個新元素添加到清單的左側(頂部),而

RPUSH

指令将一個新元素添加到清單的右側(底部)。最後,

LRANGE

指令可以從清單中按範圍提取元素。比如:

> rpush one-more-list A
(integer) 1
> rpush one-more-list B
(integer) 2
> lpush one-more-list first
(integer) 3
> lrange one-more-list 0 -1
1) "first"
2) "A"
3) "B"
           

LRANGE

指令需要另外兩個參數,要傳回的第一個元素的索引和最後一個元素的索引。如果索引為負值,Redis将從末尾開始計數,-1是清單的最後一個元素,-2是清單的倒數第二個元素,依此類推。

LPUSH

RPUSH

指令支援多個參數,可以使用一次指令添加多個元素,比如:

> rpush one-more-list 1 2 3 4 5 "last"
(integer) 9
> lrange one-more-list 0 -1
1) "first"
2) "A"
3) "B"
4) "1"
5) "2"
6) "3"
7) "4"
8) "5"
9) "last"
           

在Redis清單上,也可以移除并傳回元素。 與

LPUSH

RPUSH

指令,對應的就是

LPOP

RPOP

指令,

LPOP

指令是将清單的左側(頂部)的元素移除并傳回,

RPOP

指令是将清單的右側(底部)的元素移除并傳回。比如:

> rpush one-more-list a b c
(integer) 3
> rpop one-more-list
"c"
> rpop one-more-list
"b"
> rpop one-more-list
"a"
           

我們添加了三個元素,并移除并傳回了三個元素,此時清單為空,沒有任何元素。如果再使用

RPOP

指令,會傳回一個

NULL

值:

> rpop one-more-list
(nil)
           

RPUSH

RPOP

指令,或者

LPUSH

LPOP

指令可以實作棧的功能,使用

LPUSH

RPOP

RPUSH

LPOP

指令可以實作隊列的功能。也可以實作生産者和消費者模式,比如多個生産者使用

LPUSH

指令将任務添加到清單中,多個消費者使用

RPOP

指令将任務從清單中取出。但是,有時清單可能為空,沒有任何要處理的任務,是以

RPOP

指令僅傳回

NULL

。在這種情況下,消費者被迫等待一段時間,然後使用

RPOP

指令重試。這就暴露了有幾個缺點:

  1. 用戶端和服務端之間可以處理無用的指令,因為在清單為空時的所有請求将無法完成任何實際工作,它們隻會傳回

    NULL

  2. 由于消費者在收到

    NULL

    之後會等待一段時間,是以會增加任務處理的延遲。為了減小延遲,我們可以在兩次調用

    RPOP

    之間等待更少的時間,這就擴大了更多對Redis的無用調用。

有什麼辦法可以解決呢?使用

BRPOP

BLPOP

的指令,它們和

RPOP

LPOP

指令類似,唯一的差別是:如果清單為空時,指令會被阻塞,直到有新元素添加到清單中,或指定的逾時時間到了時,它們才會傳回到調用方。比如:

> brpop tasks 5
           

它含義是,清單為空時,等待清單中的元素,但如果5秒鐘後沒有新的元素被添加,則傳回。您可以将逾時時間傳入0,表示永遠等待元素添加。也可以傳入多個清單,這時會按參數先後順序依次檢查各個清單,傳回第一個非空清單的尾部元素。另外還有以下3點需要注意的:

  1. 當清單為空,并且有多個用戶端在等待時,有一個新的元素被添加到清單中,它會被第一個等待的用戶端擷取到,以此類推。
  2. 傳回值與

    RPOP

    指令相比有所不同,它是一個包含兩個元素的數組,包含key和對應的元素,因為

    BRPOP

    BLPOP

    指令能夠阻止等待來自多個清單的元素。
  3. 超過了逾時時間,會傳回

    NULL

清單的建立和删除都是由Redis自動完成的,當嘗試向不存在的

添加元素時,Redis會自動建立一個空的清單;當最後一個元素被移除時,Redis會自動删除這個清單。這不是特定于清單的,它适用于由多個元素組成的所有Redis資料類型,比如集合、有序集合、哈希,它們都有3條規則:

  1. 當我們将元素添加到聚合資料類型時,如果目标

    不存在,則在添加元素之前會建立一個空的聚合資料類型。比如:
> del one-more-list
(integer) 1
> lpush one-more-list 1 2 3
(integer) 3
           

但是,在

存在時,就不能操作錯誤的資料類型了,比如:

> set one-more-key OneMoreStudy
OK
> lpush one-more-key 1 2 3
(error) WRONGTYPE Operation against a key holding the wrong kind of value
> type one-more-key
string
           
  1. 當我們從聚合資料類型中删除元素時,如果該值保持為空,則key将自動銷毀。比如:
> lpush one-more-list 1 2 3
(integer) 3
> exists one-more-list
(integer) 1
> lpop one-more-list
"3"
> lpop one-more-list
"2"
> lpop one-more-list
"1"
> exists one-more-list
(integer) 0
           
  1. 當對應key不存在,并且調用隻讀指令(如

    LLEN

    指令,擷取清單長度)或寫指令(如

    LPOP

    指令)時,都會傳回空聚合資料類型的結果。比如:
> del one-more-list
(integer) 0
> llen one-more-list
(integer) 0
> lpop one-more-list
(nil)
           

Redis為了追求高性能,清單的内部實作不是一個簡單的連結清單,這裡先賣個關子,後續的文章會詳細介紹。

集合(Set)

集合是一個字元串的無序集合,

SADD

指令可以将新元素添加到集合中。還可以對集合進行許多其他操作,例如:判斷給定元素是否已存在、執行多個集合之間的交集、并集或差等等。比如:

> sadd one-more-set 1 2 3
(integer) 3
> smembers one-more-set
1) "1"
2) "3"
3) "2"
           

在上面的例子中,在集合中添加了三個元素,并讓Redis傳回所有元素。 正如你所見,傳回的元素是沒有排序的。在每次調用時,元素的順序都有可能不一樣。

還可以使用

SISMEMBER

指令判斷給定元素是否已存在,比如:

> sismember one-more-set 3
(integer) 1
> sismember one-more-set 30
(integer) 0
           

在上面的例子中,3在集合中,是以傳回1;而30不在集合中,是以傳回0。

SINTER

指令,計算出多個集合的交集;使用

SUNION

指令,計算多個集合的并集;使用

SPOP

指令,移除并傳回集合中的一個随機元素;使用

SCARD

指令,計算集合中的元素的數量。比如:

> sadd one-more-set1 1 2 3
(integer) 3
> sadd one-more-set2 2 3 4
(integer) 3
> sinter one-more-set1 one-more-set2 #交集
1) "3"
2) "2"
> sunion one-more-set1 one-more-set2 #并集
1) "1"
2) "3"
3) "2"
4) "4"
> spop one-more-set1 #随機移除一個元素
"3"
> scard one-more-set1 #元素數量
(integer) 2
           

有序集合(Sorted Set)

有序集合是一種類似于集合和哈希之間混合的資料類型。像集合一樣,有序集合中由唯一的、非重複的字元串元素組成,是以從某種意義上說,有序集合也是一個集合。但是集合中的元素是沒有排序的,而有序集合中的每個元素都與一個稱為

分數

(score)的浮點值相關聯,這就是為什麼有序集合也類似于哈希的原因,因為每個元素都映射到一個值。有序集合的排序規則如下:

  • 如果A和B是兩個具有不同分數的元素,那麼如果A.分數>B.分數,則A>B。
  • 如果A和B的分數完全相同,那麼如果A字元串在字典排序上大于B字元串,則A>B。 A和B字元串不能相等,因為有序集合中的元素都是唯一的。

我們來舉個例子,把王者榮耀戰隊的名字和積分添加到有序集合中,其中把戰隊的名字作為值,把戰隊的積分作為分數。

> zadd kpl 12 "eStarPro"
(integer) 1
> zadd kpl 12 "QGhappy"
(integer) 1
> zadd kpl 10 "XQ"
(integer) 1
> zadd kpl 8 "EDG.M"
(integer) 1
> zadd kpl 8 "RNG.M"
(integer) 1
> zadd kpl 4 "TES"
(integer) 1
> zadd kpl 2 "VG"
(integer) 1
           

如上所示,

ZADD

指令和

SADD

指令相似,但是多了一個額外的參數(在要添加的元素的前面)作為分數。

ZADD

指令也支援多個參數,雖然在上面的例子中未使用它,但你也可以指定多個分數和值對。使用有序集合,快速地傳回按其積分排序的戰隊清單,因為實際上它們已經被排序了。

需要注意的是,為了快速擷取有序集合中的元素,每次添加元素的時間複雜度都為O(log(N)),這是因為有序集合是同時使用跳躍表和字典來實作的,具體原理這裡先賣個關子,後續的文章會詳細介紹。

ZRANGE

指令按照升序擷取對應的值,比如:

> zrange kpl 0 -1
1) "VG"
2) "TES"
3) "EDG.M"
4) "RNG.M"
5) "XQ"
6) "QGhappy"
7) "eStarPro"
           

0和-1代表查詢從第一個到最後一個的元素。還可以使用

ZREVRANGE

指令按照降序擷取對應的值,比如:

> zrevrange kpl 0 -1
1) "eStarPro"
2) "QGhappy"
3) "XQ"
4) "RNG.M"
5) "EDG.M"
6) "TES"
7) "VG"
           

加上

WITHSCORES

參數,就可以連同分數一起傳回,比如:

> zrange kpl 0 -1 withscores
 1) "VG"
 2) "2"
 3) "TES"
 4) "4"
 5) "EDG.M"
 6) "8"
 7) "RNG.M"
 8) "8"
 9) "XQ"
10) "10"
11) "QGhappy"
12) "12"
13) "eStarPro"
14) "12"
           

有序集合還有更強大的功能,比如在分數範圍内操作,讓我們擷取小于10(含)的戰隊,使用

ZRANGEBYSCORE

指令:

> zrangebyscore kpl -inf 10
1) "VG"
2) "TES"
3) "EDG.M"
4) "RNG.M"
5) "XQ"
           

這就是擷取分數從負無窮到10所對應的值,同樣的我們也可以擷取分數從4到10所對應的值:

> zrangebyscore kpl 4 10
1) "TES"
2) "EDG.M"
3) "RNG.M"
4) "XQ"
           

另外有用的指令:

ZRANK

指令,它可以傳回指定值的升序排名(從0開始);

ZREVRANK

指令,它可以傳回指定值的降序排名(從0開始),比如:

> zrank kpl "EDG.M"
(integer) 2
> zrevrank kpl "EDG.M"
(integer) 4
           

有序集合的分數是随時更新的,隻要對已有的有序集合調用

ZADD

指令,就會以O(log(N))時間複雜度更新其分數和排序。這樣,當有大量更新時,有序集合是合适的。由于這種特性,常見的場景是排行榜,可以友善地顯示排名前N位的使用者和使用者在排行榜中的排名。

哈希(Hash)

Redis的哈希和人們期望的“哈希”結構是一樣的,它是一個無序哈希,内部存儲了很多鍵值對,比如:

> hmset one-more-fans:100 name Lily age 25
OK
> hget one-more-fans:100 name
"Lily"
> hget one-more-fans:100 age
"25"
> hgetall one-more-fans:100
1) "name"
2) "Lily"
3) "age"
4) "25"
           

盡管哈希很容易用來表示對象,但是實際上可以放入哈希中的字段數是沒有實際限制的,是以您可以以更多種的不同方式使用哈希。除了

HGET

指令擷取單個字段對應的值,也可以使用

HMSET

指令擷取多個字段及對應的值,它傳回的是一個數組,比如:

> hmget one-more-fans:100 name age non-existent-field
1) "Lily"
2) "25"
3) (nil)
           

HINCRBY

指令,為指定字段的值做增量,比如:

> hget one-more-fans:100 age
"25"
> hincrby one-more-fans:100 age 3
(integer) 28
> hget one-more-fans:100 age
"28"
           

Redis哈希的實作結構,和Java中的HashMap是一樣的,也是“數組+連結清單”的結構,當發生數組位置碰撞是,就會将碰撞的元素用連結清單串起來。不過Redis為了追求高性能,rehash的方式不太一樣,這裡先賣個關子,後續的文章會詳細介紹。

位圖(Bitmap)

位圖不是實際的資料類型,而是在String類型上定義的一組面向位的操作。 由于字元串是二進制安全的,并且最大長度為512MB,是以可以設定多達2^32個不同的位。位圖操作分為兩類:固定單個位操作,比如将一個位設定為1或0或擷取其值;對位組的操作,比如計算給定位範圍内設定位的數量。

位圖的最大優點之一是,它們在存儲資訊時通常可以節省大量空間。例如,在以增量使用者ID位辨別表示使用者是否要接收新聞通訊,僅使用512 MB記憶體就可以記住40億使用者的一位資訊。

SETBIT

GETBIT

指令來設定和擷取指定位,比如:

> setbit one-more-key 10 1
(integer) 0
> getbit one-more-key 10
(integer) 1
> getbit one-more-key 11
(integer) 0
           

SETBIT

指令将位号作為其第一個參數,将其設定為1或0的值作為其第二個參數。如果位号超出目前字元串長度,該指令将會自動擴大字元串。

GETBIT

指令隻是傳回指定位号的位的值,如果位号超出存儲的字元串長度則會傳回0。

對位組的操作有以下3個指令:

  1. BITOP

    指令可以在不同的字元串之間執行按位運算,提供的位運算有與、或、非和異或。
  2. BITCOUNT

    指令可以統計指定範圍内位數為1的個數。
  3. BITPOS

    指令可以查找指定範圍内為0或1的第一位。
> set one-more-key "\x13\x7f" #二進制為0001 0011 0111 1111
OK
> bitcount one-more-key #整個字元串中1的位數
(integer) 10
> bitcount one-more-key 0 0 #第一個字元(0001 0011)中1的位數
(integer) 3
> bitcount one-more-key 1 1 #第二個字元(0111 1111)中1的位數
(integer) 7
> bitpos one-more-key 0 #整個字元串中第一個0位
(integer) 0
> bitpos one-more-key 1 #整個字元串中第一個1位
(integer) 3
> bitpos one-more-key 1 0 0 #第一個字元(0001 0011)中第一個1位
(integer) 3
> bitpos one-more-key 1 1 1 #第二個字元(0111 1111)中第一個1位
(integer) 9
           

位圖可以應用于各類實時分析,也可以節省空間高效地存儲位資訊。比如,記錄使用者每天的簽到資料,每一個位表示使用者是否簽到過,這樣就可以計算出某個時間段使用者簽到了幾次,某個時間段使用者第一次簽到是哪一天。

HyperLogLogs

HyperLogLog是一種機率資料結構,用于統計唯一進制素的數量,也可以了解為估計集合中元素的個數。

通常情況下,對唯一進制素進行統計數量時,需要使用與要統計的元素數量成比例的記憶體量,因為需要記住過去已經看到的元素,以避免多次對其進行統計。但是,有一組算法可以以記憶體換取精度,最終會得到帶有标準誤差的估計數量,在Redis的HyperLogLogs中,該誤差小于1%。

這個算法的神奇之處在于,不再需要使用與所統計元素數量成比例的記憶體量,而可以使用恒定數量的記憶體。在最壞的情況下占據12KB的記憶體空間,Redis對HyperLogLog的存儲進行了優化,在計數比較少時,占據的記憶體空間會更小,這裡先賣個關子,後續的文章會詳細介紹其中原理。

在集合中,可以将每個元素添加到集合中,并使用

SCARD

指令擷取集合中的元素數量,因為

SADD

指令不會重新添加現有元素,是以元素都是唯一的。HyperLogLog的操作和集合比較類似,使用

PFADD

指令将元素添加到HyperLogLog中,類似于集合的

SADD

指令;使用

PFCOUNT

指令擷取HyperLogLog中的唯一進制素的目前近似值數量,類似于集合的

SCARD

指令。比如:

> pfadd one-more-hll a b c d e
(integer) 1
> pfcount one-more-hll 
(integer) 5
           

Redis中的HyperLogLog盡管在技術上是不同的資料結構,但被編碼為字元串,是以可以調用

GET

指令來序列化HyperLogLog,然後調用

SET

指令來将其反序列化回伺服器。

總結

Redis提供更加豐富的資料結構,鍵(Key)和字元串(String),都是二進制安全的字元串;清單(List),根據插入順序排序的字元串元素清單,基于連結清單實作;集合(Set),唯一的亂序的字元串元素的集合;有序集合(Sorted Set),與集合類似,但是每個字元串元素都與一個稱為score的數字相關聯;哈希(Hash),由字段與值相關聯組成的映射,字段和值都是字元串;位圖(Bitmap),像操作位數組一樣操作字元串值,可以設定和清除某個位,對所有為1的位進行計數,找到第一個設定1的位,找到第一個設定0的位等等;HyperLogLogs,一種機率資料結構,使用較小的記憶體空間來統計唯一進制素的數量,誤差小于1%。

作者:萬貓學社

出處:http://www.cnblogs.com/heihaozi/

版權聲明:本文遵循 CC 4.0 BY-NC-SA 版權協定,轉載請附上原文出處連結和本聲明。

微信掃描二維碼,關注

,回複「

電子書

」,免費擷取12本Java必讀技術書籍。

繼續閱讀