Redis 中的 5 種數(shù)據(jù)結(jié)構(gòu)精講(2)
命令
下面我們還是和學(xué)習(xí)其它數(shù)據(jù)類型一樣,我們還是先學(xué)習(xí)一下 Redis 列表類型的命令。
?
1.添加操作
從右邊插入元素
1 | rpush?key?value?[value?...] |
我們看 rpush 命令在插入時,是有返回值的,返回值的數(shù)量就是當(dāng)前列表中所有元素的個數(shù)。
我們也可以用下面的命令從左到右獲取當(dāng)前列表中的所有的元素,也就是如上圖所示中那樣。
1 | lrange?0?-1 |
從左邊插入元素
1 | lpush?key?value?[value?...] |
lpush 命令的返回值及用法和 rpush 命令一樣。通過上面的事例證明了我們前面說的,rpush 命令和 lpush 命令的返回值并不是當(dāng)前插入元素的個數(shù),而是當(dāng)前 key 中全部元素的個數(shù),因?yàn)楫?dāng)前 key 中已經(jīng)有了 3 個元素,所以我們在執(zhí)行插入命令時,返回的就是 6 而不是 3,。
向某個元素前或者后插入元素
1 | linsert?key?BEFORE|AFTER?pivot?value |
linsert 命令在執(zhí)行的時候首先會從當(dāng)前列表中查找到 pivot 元素,其次再將這個新元素插入到 pivot 元素的前面或者后面。并且我們通過上圖可以知道 linsert 命令在執(zhí)行成功后也是會有返回值的,返回的結(jié)果就是當(dāng)前列表中元素的個數(shù)。
?
2.查找
獲取指定范圍內(nèi)的元素列表
1 | lrange?key?start?stop |
lrange 命令會獲取列表中指定索引范圍的所有元素。
通過索引獲取列表主要有兩個特點(diǎn):
索引下標(biāo)從左到右分別是 0 到 N-1,從右到左是 -1 到 -N。
lrange 命令中的 stop 參數(shù)在執(zhí)行時會包括當(dāng)前元素,并不是所有的語言都是這樣的。我們要獲取列表中前兩個元素則可以如下圖所示:
獲取列表中指定索引下標(biāo)的元素
1 | lindex?key?index |
獲取列表長度
1 | llen?key |
?
3.刪除
從列表左側(cè)彈出元素
1 | lpop?key |
lpop 命令執(zhí)行成功后會返回當(dāng)前被刪除的元素名稱。
從列表右側(cè)彈出元素
1 | rpop?key |
rpop 命令和 lpop 命令的使用方式一樣。
刪除指定元素
1 | lrem?key?count?value |
lrem 命令會將列表中等于 value 的元素刪除掉,并且會根據(jù) count 參數(shù)來決定刪除 value 的元素個數(shù)。
下面我們看一下 count 參數(shù)的使用說明:
count > 0:表示從左到右,最多刪除 count 個元素。也就是如下圖所示:
我們看上圖中的命令中,雖然我們將 count 參數(shù)指定的是 5,將 value 參數(shù)指定的是 2,但由于當(dāng)前列表中只有一個 2,所以,當(dāng)前 lrem 命令最多只能刪除 1 個元素,并且 lrem 命令也是有返回值的,也就是當(dāng)前成功刪除元素的個數(shù)。
count < 0:從右到左,最多刪除 count 個元素。
count = 0:刪除所有元素。
按照索引范圍修剪列表
1 | ltrim?key?start?stop |
ltrim 命令會直接保留 start 索引到 stop 索引的之間的元素,并包括當(dāng)前元素,而之外的元素則都會刪除掉,所以該命令也叫修剪列表。
并且有一點(diǎn)要注意,ltrim 命令不會返回當(dāng)前的列表中元素的個數(shù),而是返回改命令是否成功的狀態(tài)。
?
4.修改
修改指定索引下標(biāo)的元素
1 | lset?key?index?value |
?
5.阻塞操作
1 2 | blpop?key?[key?...]?timeout brpop?key?[key?...]?timeout |
blpop 和 brpop 命令是 lpop 和 rpop 命令的阻塞版本,它們除了彈出方向不同以外,使用方法基本相同。
key [key …]:可以指定多個列表的鍵。
timeout:阻塞時間(單位:秒)
下面我們看一下該命令的詳細(xì)使用。
列表為空:如果 timeout=3,則表示客戶端等待 3 秒后才能返回結(jié)果,如果 timeout=0,則表示客戶端會一直等待,也就是會阻塞。
由于我期間向列表中插入了元素,否則上述命令將一直阻塞下去。
列表不為空:如果 timeout=0,并且列表不為空時,則 blpop 和 brpop 命令會立即返回結(jié)果,不會阻塞。
下面我們看一下 blpop 和 brpop 命令的注意事項(xiàng)。
如果使用 blpop 和 brpop 命令指定多個鍵時,blpop 和 brpop 命令會從左到右遍歷鍵,并且一旦有一個鍵能返回元素,則客戶端會立即返回。
當(dāng)列表為空時,上述命令會阻塞,如果向上述中的任何一個鍵中插入元素,則上述命令會直接返回該鍵的元素。
如果多個客戶端都對同一個鍵執(zhí)行 blpop 或者 brpop 命令,則最先執(zhí)行該命令的客戶端會獲取到該鍵的元素。
我同時啟動了 3 個客戶端,因?yàn)楫?dāng)前列表為空,所以上述命令執(zhí)行后會阻塞。如果此時我向該列表中插入元素,則只有第一個客戶端會有返回結(jié)果,因?yàn)榈谝粋€客戶端是第一個執(zhí)行上述命令的。
?
時間復(fù)雜度
下面我們看一下列表中命令的相關(guān)時間復(fù)雜度。
操作類型 | 命令 | 時間復(fù)雜度 |
添加 | rpush key value [value …] | O(k),k是元素的個數(shù) |
添加 | lpush key value [value …] | O(k),k是元素的個數(shù) |
添加 | linsert key BEFORE/AFTER pivot value | O(n),n是pivot距離列表頭或者尾的距離 |
查找 | lrange key start stop | O(s + n),s是start偏移量,n是start到stop的范圍 |
查找 | lindex key index | O(n),n是索引的偏移量 |
查找 | llen key | O(1) |
刪除 | lpop key | O(1) |
刪除 | rpop key | O(1) |
刪除 | lrem key count value | O(n),n是列表長度 |
刪除 | ltrim key start stop | O(n),n是要裁剪的元素個數(shù) |
修改 | lset key index value | O(n),n是索引的偏移量 |
阻塞操作 | blpop/brpop key [key …] timeout | O(1) |
?
內(nèi)部編碼
列表中的內(nèi)部編碼有兩種,它們分別是:
ziplist(壓縮列表):當(dāng)列表中元素個數(shù)小于 512(默認(rèn))個,并且列表中每個元素的值都小于 64(默認(rèn))個字節(jié)時,Redis 會選擇用 ziplist 來作為列表的內(nèi)部實(shí)現(xiàn)以減少內(nèi)存的使用。當(dāng)然上述默認(rèn)值也可以通過相關(guān)參數(shù)修改:list-max-ziplist-entried(元素個數(shù))、list-max-ziplist-value(元素值)。
linkedlist(鏈表):當(dāng)列表類型無法滿足 ziplist 條件時,Redis 會選擇用 linkedlist 作為列表的內(nèi)部實(shí)現(xiàn)。
?
集合類型
Redis 中的集合類型,也就是 set。在 Redis 中 set 也是可以保存多個字符串的,經(jīng)常有人會分不清 list 與 set,下面我們重點(diǎn)介紹一下它們之間的不同:
set 中的元素是不可以重復(fù)的,而 list 是可以保存重復(fù)元素的。
set 中的元素是無序的,而 list 中的元素是有序的。
set 中的元素不能通過索引下標(biāo)獲取元素,而 list 中的元素則可以通過索引下標(biāo)獲取元素。
除此之外 set 還支持更高級的功能,例如多個 set 取交集、并集、差集等。
?
命令
下面我們介紹一下 set 中的相關(guān)命令。
?
1.集合內(nèi)操作
添加元素
1 | sadd?key?member?[member?...] |
sadd 命令也是有返回值的,它的返回值就是當(dāng)前執(zhí)行 sadd 命令成功添加元素的個數(shù),因?yàn)?set 中不能保存重復(fù)元素,所以在執(zhí)行?sadd setkey c d?命令時,返回的是 1,而不是 2。因?yàn)樵?c 已經(jīng)成功保存到 set 中,不能再保存了,只能將 d 保存到 set 中。
刪除元素
1 | srem?key?member?[member?...] |
srem 命令和 sadd 命令一樣也是有返回值的,返回值就是當(dāng)前刪除元素的個數(shù)。
計(jì)算元素個數(shù)
1 | scard?key |
scard 命令的時間復(fù)雜度為O(1),scard 命令不會遍歷 set 中的所有元素,而是直接使用 Redis 中的內(nèi)部變量。
判讀元素是否在集合中
1 | sismember?key?member |
sismember 命令也有返回值,如果返回值為1則表示當(dāng)前元素在當(dāng)前 set 中,如果返回 0 則表示當(dāng)前元素不在 set 中。
隨機(jī)從 set 中返回指定個數(shù)元素
1 | srandmember?key?[count] |
srandmember 命令中有一個可選參數(shù) count,count 參數(shù)指的是返回元素的個數(shù),如果當(dāng)前 set 中的元素個數(shù)小于 count,則 srandmember 命令返回當(dāng)前 set 中的所有元素,如果 count 參數(shù)等于 0,則不返回任何數(shù)據(jù),如果 count 參數(shù)小于 0,則隨機(jī)返回當(dāng)前 count 個數(shù)的元素。
從集合中隨機(jī)彈出元素
1 | spop?key?[count] |
spop 命令也是隨機(jī)從 set 中彈出元素,并且也支持 count 可選參數(shù),但有一點(diǎn)和 srandmember 命令不同。spop 命令在隨機(jī)彈出元素之后,會將彈出的元素從 set 中刪除。
獲取所有元素
1 | smembers?key |
smembers 命令雖然能獲取當(dāng)前 set 中所有的元素,但返回元素的順序與 sadd 添加元素的順序不一定相同,這也就是前面提到過的保存在 set 中的元素是無序的。
?
2.集合間操作
集合的交集
1 | sinter?key?[key?...] |
集合的并集
1 | sunion?key?[key?...] |
集合的差集
1 | sdiff?key?[key?...] |
將集合的交集、并集、差集的結(jié)果保存
1 2 3 | sinterstore?destination?key?[key?...] sunionstore?destination?key?[key?...] sdiffstore?destination?key?[key?...] |
為什么 Redis 要提供 sinterstore、sunionstore、sdiffstore 命令來將集合的交集、并集、差集的結(jié)果保存起來呢?這是因?yàn)?Redis 在進(jìn)行上述比較時,會比較耗費(fèi)時間,所以為了提高性能可以將交集、并集、差集的結(jié)果提前保存起來,這樣在需要使用時,可以直接通過 smembers 命令獲取。
?
時間復(fù)雜度
下面我們看一下 set 中相關(guān)命令的時間復(fù)雜度。
命令 | 時間復(fù)雜度 |
sadd key member [member …] | O(k),k是元素的個數(shù) |
srem key member [member …] | O(k),k是元素的個數(shù) |
scard key | O(1) |
sismember key member | O(1) |
srandmember key [count] | O(count) |
spop key [count] | O(1) |
smembers key | O(n),n是元素的總數(shù) |
sinter key [key …] | O(m * k),k是多個集合中元素最少的個數(shù),m是鍵個數(shù) |
sunion key [key …] | O(k),k是多個元素個數(shù)和 |
sdiff key [key …] | O(k),k是多個元素個數(shù)和 |
sinterstore destination key [key …] | O(m * k),k是多個集合中元素最少的個數(shù),m是鍵個數(shù) |
sunionstore destination key [key …] | O(k),k是多個元素個數(shù)和 |
sdiffstore destination key [key …] | O(k),k是多個元素個數(shù)和 |
?
內(nèi)部編碼
intset(整數(shù)集合):當(dāng)集合中的元素都是整數(shù),并且集合中的元素個數(shù)小于 512 個時,Redis 會選用 intset 作為底層內(nèi)部實(shí)現(xiàn)。
hashtable(哈希表):當(dāng)上述條件不滿足時,Redis 會采用 hashtable 作為底層實(shí)現(xiàn)。
備注:我們可以通過 set-max-intset-entries 參數(shù)來設(shè)置上述中的默認(rèn)參數(shù)。
下面我們看一下具體的事例,來驗(yàn)證我們上面提到的內(nèi)部編碼。
當(dāng)元素個數(shù)較少并且都是整數(shù)時,內(nèi)部編碼為 intset。
當(dāng)元素不全是整數(shù)時,內(nèi)部編碼為 hashtable。
當(dāng)元素個數(shù)超過 512 個時,內(nèi)部編碼為 hashtable。
1 2 3 4 5 6 7 8 9 10 11 | import?redis ? r?=?redis.Redis(host='127.0.0.1',?port=6379) ????if?r.object('encoding',?'setkey')?!=?None:???? ????print('Key為【setkey】的字節(jié)編碼為【%s】'?%?r.object('encoding',?'setkey').decode('utf-8')) for?i?in?range(1,?600): ????r.sadd('setkey',?i) if?r.object('encoding',?'setkey')?!=?None: ????print('Key為【setkey】的字節(jié)編碼為【%s】'?%?r.object('encoding',?'setkey').decode('utf-8')) Key為【setkey】的字節(jié)編碼為【intset】 Key為【setkey】的字節(jié)編碼為【hashtable】 |
?
有序集合類型
看名字我們就知道,有序集合也是一種集合,并且這個集合還是有序的。列表也是有序的,那它和有序集合又有什么不同呢?有序集合的有序和列表的有序是不同的。列表中的有序指的的是插入元素的順序和查詢元素的順序相同,而有序集合中的有序指的是它會為每個元素設(shè)置一個分?jǐn)?shù)(score),而查詢時可以通過分?jǐn)?shù)計(jì)算元素的排名,然后再返回結(jié)果。因?yàn)橛行蚣弦彩羌项愋?,所以有序集合中也是不插入重?fù)元素的,但在有序集合中分?jǐn)?shù)則是可以重復(fù),那如果在有序集合中有多個元素的分?jǐn)?shù)是相同的,這些重復(fù)元素的排名是怎么計(jì)算的呢?后邊我們再做詳細(xì)說明。
下面先看一下列表、集合、有序集合三種數(shù)據(jù)類型之間的區(qū)別:
數(shù)據(jù)結(jié)構(gòu) | 是否允許重復(fù)元素 | 是否有序 | 有序?qū)崿F(xiàn)方式 | 應(yīng)用場景 |
列表 | 是 | 是 | 索引下標(biāo) | 時間軸、消息隊(duì)列 |
集合 | 否 | 否 | 無 | 標(biāo)簽、社交 |
有序集合 | 否 | 是 | 分?jǐn)?shù) | 排行榜、社交 |
?
命令
?
1.集合內(nèi)操作
添加元素
1 | zadd?key?[NX|XX]?[CH]?[INCR]?score?member?[score?member?...] |
zadd 命令也是有返回值的,返回值就是當(dāng)前 zadd 命令成功添加元素的個數(shù)。zadd 命令有很多選填參數(shù):
nx: 元素必須不存在時,才可以設(shè)置成功。
xx: 元素必須存在時,才可以設(shè)置成功。
ch: 返回此命令執(zhí)行完成后,有序集合元素和分?jǐn)?shù)發(fā)生變化的個數(shù)
incr: 對 score 做增加。
備注:由于有序集合相比集合提供了排序字段,正是因?yàn)槿绱艘哺冻隽讼鄳?yīng)的代價(jià),sadd 的時間復(fù)雜度為 O(1),而 zadd 的時間復(fù)雜度為O(log(n))。
計(jì)算成員個數(shù)
1 | zcard?key |
計(jì)算某個成員的分?jǐn)?shù)
1 | zscore?key?member |
在使用 zscore 命令時,如果 key 不存在,或者元素不存在時,該命令返回的都是(nil)。
計(jì)算成員的排名
1 2 | zrank?key?member zrevrank?key?member |
zrank 命令是從分?jǐn)?shù)低到高排名,而 zrevrank 命令則恰恰相反,從高到低排名。有一點(diǎn)要特別注意, zrank 和 zrevrank 命令與 zscore 是命令不同的,前者通過分?jǐn)?shù)計(jì)算出最后的排名,而后者則是直接返回當(dāng)前元素的分?jǐn)?shù)。
刪除元素
1 | zrem?key?member?[member?...] |
返回的結(jié)果為成功刪除元素的個數(shù),因?yàn)?zrem 命令是支持批量刪除的。
增加元素分?jǐn)?shù)
1 | zincrby?key?increment?member |
雖然 zincrby 命令是增加元素分?jǐn)?shù)的,但我們也可以指定負(fù)數(shù),這樣當(dāng)前元素的分?jǐn)?shù),則會相減。
返回指定排名范圍的元素
1 2 | zrange?key?start?stop?[WITHSCORES] zrevrange?key?start?stop?[WITHSCORES] |
zrange 命令是通過分?jǐn)?shù)從低到高返回?cái)?shù)據(jù),而 zrevrange 命令是通過分?jǐn)?shù)從高到低返回?cái)?shù)據(jù)。如果執(zhí)行命令時添加了 WITHSCORES 可選參數(shù),則返回?cái)?shù)據(jù)時會返回當(dāng)前元素的分?jǐn)?shù)。
返回指定分?jǐn)?shù)范圍的元素
1 2 | zrangebyscore?key?min?max?[WITHSCORES]?[LIMIT?offset?count] zrevrangebyscore?key?max?min?[WITHSCORES]?[LIMIT?offset?count] |
min 和 max 參數(shù)還支持開區(qū)間(小括號)和閉區(qū)間(中括號),同時我們還可以用 -inf 和 +inf 參數(shù)代表無限小和無限大。
返回指定分?jǐn)?shù)范圍元素個數(shù)
1 | zcount?key?min?max |
刪除指定排名內(nèi)的升序元素
1 | zremrangebyrank?key?start?stop |
刪除指定分?jǐn)?shù)范圍元素
1 | zremrangebyscore?key?min?max |
?
2.集合間操作
交集
1 | zinterstore?destination?numkeys?key?[key?...]?[WEIGHTS?weight]?[AGGREGATE?SUM|MIN|MAX] |
zinterstore 命令參數(shù)比較多:
destination:將交集的計(jì)算結(jié)果,保存到這個鍵中。
numkeys:需要做交集計(jì)算鍵的個數(shù)。
key [key …]:需要做交集計(jì)算的鍵。
WEIGHTS weight:每個鍵的權(quán)重,在做交集計(jì)算時,每個鍵中的分?jǐn)?shù)值都會乘以這個權(quán)重,默認(rèn)每個鍵的權(quán)重為 1。
AGGREGATE SUM|MIN|MAX:計(jì)算成員交集后,分值可以按照 sum(和)、min(最小值)、max(最大值)做匯總,默認(rèn)值為? sum。
下面我們將權(quán)重設(shè)置為 0.5,這樣當(dāng)計(jì)算交集后,有序集合中的元素分?jǐn)?shù)將都會減半,并且使用 max 參數(shù)匯總。
并集
1 | zunionstore?destination?numkeys?key?[key?...]?[WEIGHTS?weight]?[AGGREGATE?SUM|MIN|MAX] |
zunionstore 命令的相關(guān)參數(shù)和 zinterstore 命令相同。
?
時間復(fù)雜度
命令 | 時間復(fù)雜度 |
zadd key [NX/XX] [CH] [INCR] score member [score member …] | O(k*log(n)),k是添加元素的個數(shù),n是當(dāng)前有序集合元素個數(shù) |
zcard key | O(1) |
zscore key member | O(1) |
zrank key member | O(log(n)),n是當(dāng)前有序集合元素個數(shù) |
zrevrank key member | O(log(n)),n是當(dāng)前有序集合元素個數(shù) |
zrem key member [member …] | O(k*log(n)),k是刪除元素的個數(shù),n是當(dāng)前有序集合元素個數(shù) |
zincrby key increment member | O(log(n)),n是當(dāng)前有序集合元素個數(shù) |
zrange key start stop [WITHSCORES] | O(log(n) + k),k是要獲取的元素個數(shù),n是當(dāng)前有序集合個數(shù) |
zrevrange key start stop [WITHSCORES] | O(log(n) + k),k是要獲取的元素個數(shù),n是當(dāng)前有序集合個數(shù) |
zrangebyscore key min max [WITHSCORES] [LIMIT offset count] | O(log(n) + k),k是要獲取的元素個數(shù),n是當(dāng)前有序集合個數(shù) |
zrevrangebyscore key max min [WITHSCORES] [LIMIT offset count] | O(log(n) + k),k是要獲取的元素個數(shù),n是當(dāng)前有序集合個數(shù) |
zcount key min max | O(log(n)),n是當(dāng)前有序集合元素個數(shù) |
zremrangebyrank key start stop | O(log(n) + k),k是要刪除的元素個數(shù),n是當(dāng)前有序集合個數(shù) |
zremrangebyscore key min max | O(log(n) + k),k是要刪除的元素個數(shù),n是當(dāng)前有序集合個數(shù) |
zinterstore destination numkeys key [key …] [WEIGHTS weight] [AGGREGATE SUM/MIN/MAX] | O(n?k) + O(m?log(m)),n是元素元素最小的有序集合元素個數(shù),k是有序集合個數(shù),m是結(jié)果集中元素個數(shù) |
zunionstore destination numkeys key [key …] [WEIGHTS weight] [AGGREGATE SUM/MIN/MAX] | O(n) + O(m * log(m)),n是所有有序集合元素個數(shù)和,m是結(jié)果集中元素個數(shù)。 |
?
內(nèi)部編碼
有序集合類型的內(nèi)部編碼有兩種,它們分別是:
ziplist(壓縮列表):當(dāng)有序集合的元素個數(shù)小于 128 個(默認(rèn)設(shè)置),同時每個元素的值都小于 64 字節(jié)(默認(rèn)設(shè)置),Redis 會采用 ziplist 作為有序集合的內(nèi)部實(shí)現(xiàn)。
skiplist(跳躍表):當(dāng)上述條件不滿足時,Redis 會采用 skiplist 作為內(nèi)部編碼。
備注:上述中的默認(rèn)值,也可以通過以下參數(shù)設(shè)置:zset-max-ziplist-entries 和 zset-max-ziplist-value。
下面我們用以下示例來驗(yàn)證上述結(jié)論。
當(dāng)元素個數(shù)比較少,并且每個元素也比較小時,內(nèi)部編碼為 ziplist:
當(dāng)元素個數(shù)超過 128 時,內(nèi)部編碼為 skiplist。下面我們將采用 python 動態(tài)創(chuàng)建128個元素,下面為源碼:
1 2 3 4 5 6 7 8 9 10 11 | import?redis ? r?=?redis.Redis(host='127.0.0.1',?port=6379) if?r.object('encoding',?'zsetkey')?!=?None: ????print('Key為【zsetkey】的字節(jié)編碼為【%s】'?%?r.object('encoding',?'zsetkey').decode('utf-8')) for?i?in?range(1,?600): ????r.zadd('zsetkey',i,1) if?r.object('encoding',?'zsetkey')?!=?None: ????print('Key為【zsetkey】的字節(jié)編碼為【%s】'?%?r.object('encoding',?'zsetkey').decode('utf-8')) Key為【zsetkey】的字節(jié)編碼為【ziplist】 Key為【zsetkey】的字節(jié)編碼為【skiplist】 |
當(dāng)有序集合中有任何一個元素大于 64 個字節(jié)時,內(nèi)部編碼為 skiplist。
1 2 3 4 5 6 7 8 9 10 11 12 | import?redis ? r?=?redis.Redis(host='127.0.0.1',?port=6379) if?r.object('encoding',?'zsetkey')?!=?None: ????print('Key為【zsetkey】的字節(jié)編碼為【%s】'?%?r.object('encoding',?'zsetkey').decode('utf-8')) value?=?'' for?i?in?range(1,?600): ????value?+=?str(i) r.zadd('zsetkey',value,1) if?r.object('encoding',?'zsetkey')?!=?None: ????print('Key為【zsetkey】的字節(jié)編碼為【%s】'?%?r.object('encoding',?'zsetkey').decode('utf-8')) Key為【zsetkey】的字節(jié)編碼為【skiplist】 |