天天看點

redis之學習(二)使用場景以及性能分析String類型哈希(hash)清單(list)集合(set)有序集合

之前我們以及了解到redis的介紹與簡單用法,本文将從各個資料結構的性能與操作redis名字的複雜度進行實際生産中的分析。

String類型

操作字元串類型指令的時間複雜度
指令 時間複雜度
set key value O(1)
get key O(1)
del key [key …] O(k),k為key的個數
mset key value [key value …] O(k),k為key的個數
mget key [key …] O(k),k為key的個數
incr key O(1)
decr key O(1)
incrby key increment O(1)
decrby key increment O(1)
incrbyfloat key increment O(1)
append key value O(1)
strlen key O(1)
setrange key offset value O(1)
getrange key start end O(n),n是字元串的長度,由于擷取字元串非常快,是以如果字元串不是很長,可以視為O(1)
字元串類型的内部編碼有三種:
  • int 8個位元組的長整型
  • embstr 小于等于39個位元組的字元串 (redis版本3.0之前是39,redis3.2之後是44位元組)
  • raw 大于39個字元串的字元串(redis版本3.0之前是39,redis3.2之後是44位元組)

    redis會根據目前值的類型和長度決定使用哪種内部編碼實作。

int類型:

127.0.0.1:6379> set test:int 15
OK
127.0.0.1:6379> get test:int
"15"
127.0.0.1:6379> type test:int
string
127.0.0.1:6379> object encoding test:int
"int"
           

embstr類型字元串長度小于等于39:

127.0.0.1:6379> set test:str hello
OK
127.0.0.1:6379> get test:str
"hello"
127.0.0.1:6379> object encoding test:str
"embstr"
           

當字元串長度大于39(3.0之前,3.2之後是44)的時候會變成raw,如下:

127.0.0.1:6379> set test:raw 12345678912345678912345678912345678912345678
OK
127.0.0.1:6379> strlen test:raw                      #我用的是redis5.0故是44
(integer) 44
127.0.0.1:6379> object encoding test:raw
"embstr"
127.0.0.1:6379> set test:raw 123456789123456789123456789123456789123456789
OK
127.0.0.1:6379> strlen test:raw
(integer) 45
127.0.0.1:6379> object encoding test:raw           #當長度大于44時候就轉變成了raw
"raw"
           

适用場景

  1. 緩存功能。Redis支援高并發,是以使用redis做緩存層可以加速讀寫與降低後端壓力
  2. 計數 Redis提供的incr指令可以實作快速的計數、查詢緩存。同時redis 的資料可以異步到其他的資料源
  3. 共享session 。 一個分布式的web服務,如果将使用者的session資訊存儲在各自的伺服器上,處于負載均衡考慮分布式服務會将使用者的通路均衡到不同伺服器上,使用者重新整理一次可能就會需要重新登陸,這是非常不合理的。故使用redis将使用者的session進行集中管理,每次使用者更新或者查詢登陸資訊都可以在redis中擷取。單點登入就是這種做法。
  4. 限速 。一些應用處于安全考慮,在每次進行登陸的時候輸入手機驗證碼确定使用者是否是本人,但短信接口不可以頻繁通路,利用redis可以很優雅的解決這個問題, redis提供的過期時間可以完美的限制ip、接口等在某一段時間内的 通路次數的限制。

哈希(hash)

操作hash指令的時間複雜度:

指令 時間複雜度
hset key field value O(1)
hget key field O(1)
hdel key field [field …] O(k),k是field的數量
hlen key O(1)
hgetall key O(n),n是field 的個數
hmget field [ field …] O(k),k是field的個數
hmset field value[ field value …] O(k),k是field的個數
hexists key field O(1)
hkeys key O(n),n是field的總數
hvals key O(n),n是filed的總數
hsetnx key field value O(1)
hincrby key field increment O(1)
hincrbyfloat key field increment O(1)
hstrlen key field O(1)
哈希類型的内部編碼:
  • ziplist(壓縮清單),當哈希類型元素個數小于hash-max-ziplist-entries配置(預設512個)同時所有的值都小于hash-max-ziplist-value配置(預設64位元組)時。redis會使用ziplist作為哈希的内部實作。ziplist使用更加緊湊的結構實作多個元素的連續存儲,在節省記憶體方面比hashtable更加優秀。
  • hashtable(哈希表)。當哈希類型無法滿足ziplist條件的時候,Redis會使用hashtable作為哈希的内部實作,因為此時ziplist的讀寫效率會下降,而hashtable的讀寫時間複雜為O(1)。

以下例子可以看到當數量超過512時候就換轉換為hashtable

127.0.0.1:6379> hlen hash:test1
(integer) 512
127.0.0.1:6379> hlen hash:test2
(integer) 512
127.0.0.1:6379> object encoding hash:test1
"ziplist"
127.0.0.1:6379> object encoding hash:test2
"ziplist"
127.0.0.1:6379> hset hash:test1 field_513 513
(integer) 1
127.0.0.1:6379> hlen hash:test1
(integer) 513
127.0.0.1:6379> object encoding hash:test1
"hashtable"
           

當其中某一個元素大小超過64位元組的時候就會發現由ziplist轉換為了hashtable

127.0.0.1:6379> hlen hash:test2
(integer) 512
127.0.0.1:6379> hget hash:test2 field_1
"1"
127.0.0.1:6379> set test:len "1234567891234567891234567891234567891234567891234561234567891234"
OK
127.0.0.1:6379> strlen test:len
(integer) 64
127.0.0.1:6379> hset hash:test2 field_1 "1234567891234567891234567891234567891234567891234561234567891234"
(integer) 0
127.0.0.1:6379> object encoding hash:test2
"ziplist"
127.0.0.1:6379> set test:len "12345678912345678912345678912345678912345678912345612345678912345"
OK
127.0.0.1:6379> strlen test:len
(integer) 65
127.0.0.1:6379> hset hash:test2 field_1 "12345678912345678912345678912345678912345678912345612345678912345"
(integer) 0
127.0.0.1:6379> object encoding hash:test2
"hashtable"

           

适用場景

緩存關系型資料。使用hash存儲此類型的資料相比較字元串序列化存儲資訊更為直覺。例如存儲一個使用者的登陸資訊。但需要注意一下兩點:

  • 哈希類型是稀疏的,而關系型資料庫是完全結構化的
  • 關系型資料庫可以做複雜的關系查詢,而Redis去模拟關系型複雜查詢開發困難,維護成本較高。

    對于同樣緩存使用者資訊有三種方案,各有優缺點,故實際開發應該結合場景去使用

  1. 原生字元串類型,每個屬性一個鍵。此種方式簡單直覺,每個屬性都支援更新操作,但是會占用過的的鍵與記憶體,内聚性比較差
  2. 序列化字元串類型。它可以簡化程式設計,合理的序列化可以提高記憶體的使用。但是序列化與反序列化有一定的開銷(相比較而言,開銷很大),同時每次更新資料都需要進行将資料取出進行反序列化與序列化。
  3. 哈希類型。使用者的資訊屬性使用field-value表示,但隻用一個key儲存。它簡單直覺,合理使用可以減少記憶體的使用。但需要控制ziplist與hashtable的轉換,hashtable會消耗更多的記憶體。

清單(list)

操作list指令的時間複雜度

指令 時間複雜度
rpush key value [value …] O(k),k是元素個數
lpush key value [value …] O(k),k是元素個數
linsert key before|after pivot value O(n),n是pivot距離清單頭或尾的距離
lrange key start end O(s+n),s是start偏移量,n是start到end的範圍
lindex key index O(n),n是索引的偏移量
llen key O(1)
lpop key O(1)
rpop key O(1)
lremkey count value O(n),n是清單的長度
ltrim key start end O(n),n是要裁減的長度
lset key index value O(n),n是索引的偏移量
blpop brpp O(1)

list的内部編碼

  • ziplist(壓縮清單),當list類型元素個數小于lit-max-ziplist-entries配置(預設512個)同時所有的值都小于list-max-ziplist-value配置(預設64位元組)時。redis會使用ziplist作為list的内部實作。
  • linkedlist(連結清單)。當清單類型無法滿足ziplist條件的時候,Redis會使用linkedlist作為清單的内部實作。
  • quicklist。它是以一個ziplist為節點的linkedlist,它結合了ziplist和linkedlist兩者的優勢。這是redis3.2之後提供的,在redis3.2之後redis會使用這種類型當作list的内部編碼。

适用場景

  1. 消息隊列。redis的lpush+brpop指令組合可以實作阻塞隊列,生産者用戶端使用lpush從清單左側插入元素,多個消費者使用brpop指令阻塞式的“搶”清單尾部的元素,多個用戶端保證了消息的負載均衡與高可用性。
  2. 文章清單。使用者屬于自己的文章清單,當需要分頁展示的時候可以考慮list。因為清單不但是有序的,而且支援按照索引範圍的擷取元素。

小提示:實際上清單的應用場景非常多,不僅僅是上述舉例的兩個。在具體使用的時候可以參考以下技巧:

  • lpush + lpop = Stack(棧)
  • lpush + rpop = Queue(隊列)
  • lpush + ltrim = Capped Collection(有限集合)
  • lpush + brpop = message Queue(消息隊列)

集合(set)

操作set指令的時間複雜度

指令 時間複雜度
sadd key element [element …] O(k),k是元素個數
srem key element [element …] O(k),k是元素個數
scard key O(1)
sismember key element O(1)
srandmember key [count] O(count)
spop key O(1)
smembers key O(n),n是元素總個數
sinter key [key …] 或者 sinterstore O(m*k),k是多個集合中元素最少的個數,m是鍵的個數
suinon key [key] 或者 suionstore O(k),k是多個集合元素個數和
sdiff key [key …] 或者 sdiffstore O(k),k是多個集合元素個數的和

内部編碼

  • intset(整數集合)。當集合中的元素都是整數且元素個數小于set-max-intset-entries配置(預設512個)時,redis會選用intset來作為集合的内部實作。減少記憶體使用
  • hashtable(哈希表)。當集合類型無法滿足intset的條件的時候,redis會使用hashtable作為集合的内部實作。
127.0.0.1:6379> scard set:test1
(integer) 512
127.0.0.1:6379> object encoding set:test1
"intset"
127.0.0.1:6379> sadd set:test1 513
(integer) 1
127.0.0.1:6379> SCARD set:test1
(integer) 513                //當數量超過512就轉為了hashtable
127.0.0.1:6379> object encoding set:test1
"hashtable"
           
127.0.0.1:6379> sadd set:test2 1 2 3
(integer) 3
127.0.0.1:6379> scard set:test2
(integer) 3
127.0.0.1:6379> object encoding set:test2
"intset"
127.0.0.1:6379> sadd set:test2 hello        //當加入一個字元串類型的資料時轉化為hashtable
(integer) 1
127.0.0.1:6379> scard set:test2
(integer) 4
127.0.0.1:6379> object encoding set:test2
"hashtable"
           

适用場景

  • 标簽。set集合類典型的應用場景就是标簽(tag),例如一個使用者可能對娛樂、體育感興趣,另一個對曆史、新聞感興趣。這些興趣點就标簽,有了這些資料就可以得到喜歡某一事物的人。這些資料對于使用者體驗以及增強使用者黏度比較重要。set集合類的并交差集非常易于做這些事情。

有序集合

操作有序集合(zset)指令的時間複雜度

指令 時間複雜度
zadd key score membeer [score member …] O(k*log(n)),k是添加成員的個數,n是目前有序集合的個數
zcard key O(1)
zscore key member O(1)
zrank key member | zrevrank key member O(log(n)),n是目前有序集合成員個數
zrem key member [member …] O(k*long(n)),k是删除成員個數,n是目前有序集合成員個數
zincrby key increment member O(log(n)),n是目前有序集合的成員個數
zrange key start end [witthscores] | zrevrange key start end [withscores] O(log(n)+k),n是目前集合成員個數,k是要擷取的成員個數
zrangebyscore key min max [withscores] | zrevrangebyscore key max min [withscores] O(log(n)+k),n是目前集合成員個數,k是要擷取的成員個數
zcount O(log(n)),n是目前有序集合成員個數
zremrangebyrank key start end O(log(n)+k),k是要删除的成員個數,n是目前有序集合成員個數
zremrangebyscore key min max O(log(n)+k),k是要删除的成員個數,n是目前有序集合成員個數

内部編碼

  • ziplist(壓縮清單)。當有序集合的元素個數小于zset-max-ziplist-entries配置(預設128個),同時所有的元素的值都小于zset-max-ziplist-value配置(預設64位元組)時,redis會使用ziplist作為有序集合的内部實作,減少記憶體使用。
  • skiplist(跳躍表)。當ziplist條件不滿足的時候,會使用skiplist。

适用場景:

  • 排行榜系統