Redis有5個基本資料結構,string、list、hash、set和zset。它們是日常開發中使用頻率非常高應用最為廣泛的資料結構,把這5個資料結構都吃透了,你就掌握了Redis應用知識的一半了。
string
首先我們從string談起。string表示的是一個可變的位元組數組,我們初始化字元串的内容、可以拿到字元串的長度,可以擷取string的字串,可以覆寫string的字串内容,可以追加子串。
Redis的字元串是動态字元串,是可以修改的字元串,内部結構實作上類似于Java的ArrayList,采用預配置設定備援空間的方式來減少記憶體的頻繁配置設定,如圖中所示,内部為目前字元串實際配置設定的空間capacity一般要高于實際字元串長度len。當字元串長度小于1M時,擴容都是加倍現有的空間,如果超過1M,擴容時一次隻會多擴1M的空間。需要注意的是字元串最大長度為512M。
初始化字元串 需要提供「變量名稱」和「變量的内容」
> set ireader beijing.zhangyue.keji.gufen.youxian.gongsi
OK
複制代碼
擷取字元串的内容 提供「變量名稱」
> get ireader
"beijing.zhangyue.keji.gufen.youxian.gongsi"
擷取字元串的長度 提供「變量名稱」
> strlen ireader
(integer) 42
擷取子串 提供「變量名稱」以及開始和結束位置[start, end]
> getrange ireader 28 34
"youxian"
覆寫子串 提供「變量名稱」以及開始位置和目标子串
> setrange ireader 28 wooxian
(integer) 42 # 傳回長度
"beijing.zhangyue.keji.gufen.wooxian.gongsi"
追加子串
> append ireader .hao
(integer) 46 # 傳回長度
"beijing.zhangyue.keji.gufen.wooxian.gongsi.hao"
遺憾的是字元串沒有提供字串插入方法和子串删除方法。
計數器 如果字元串的内容是一個整數,那麼還可以将字元串當成計數器來使用。
> set ireader 42
"42"
> incrby ireader 100
(integer) 142
"142"
> decrby ireader 100
> incr ireader # 等價于incrby ireader 1
(integer) 143
> decr ireader # 等價于decrby ireader 1
計數器是有範圍的,它不能超過Long.Max,不能低于Long.MIN
> set ireader 9223372036854775807
> incr ireader
(error) ERR increment or decrement would overflow
> set ireader -9223372036854775808
> decr ireader
過期和删除 字元串可以使用del指令進行主動删除,可以使用expire指令設定過期時間,到點會自動删除,這屬于被動删除。可以使用ttl指令擷取字元串的壽命。
> expire ireader 60
(integer) 1 # 1表示設定成功,0表示變量ireader不存在
> ttl ireader
(integer) 50 # 還有50秒的壽命,傳回-2表示變量不存在,-1表示沒有設定過期時間
> del ireader
(integer) 1 # 删除成功傳回1
(nil) # 變量ireader沒有了
list
Redis将清單資料結構命名為list而不是array,是因為清單的存儲結構用的是連結清單而不是數組,而且連結清單還是雙向連結清單。因為它是連結清單,是以随機定位性能較弱,首尾插入删除性能較優。如果list的清單長度很長,使用時我們一定要關注連結清單相關操作的時間複雜度。
負下标 連結清單元素的位置使用自然數0,1,2,....n-1表示,還可以使用負數-1,-2,...-n來表示,-1表示「倒數第一」,-2表示「倒數第二」,那麼-n就表示第一個元素,對應的下标為0。
隊列/堆棧 連結清單可以從表頭和表尾追加和移除元素,結合使用rpush/rpop/lpush/lpop四條指令,可以将連結清單作為隊列或堆棧使用,左向右向進行都可以
# 右進左出
> rpush ireader go
(integer) 1
> rpush ireader java python
(integer) 3
> lpop ireader
"go"
"java"
"python"
# 左進右出
> lpush ireader go java python
> rpop ireader
...
# 右進右出
> rpush ireader go java python
> rpop ireader
# 左進左出
在日常應用中,清單常用來作為異步隊列來使用。
長度 使用llen指令擷取連結清單長度
> llen ireader
随機讀 可以使用lindex指令通路指定位置的元素,使用lrange指令來擷取連結清單子元素清單,提供start和end下标參數
> lindex ireader 1
> lrange ireader 0 2
1) "go"
2) "java"
3) "python"
> lrange ireader 0 -1 # -1表示倒數第一
使用lrange擷取全部元素時,需要提供end_index,如果沒有負下标,就需要首先通過llen指令擷取長度,才可以得出end_index的值,有了負下标,使用-1代替end_index就可以達到相同的效果。
修改元素 使用lset指令在指定位置修改元素。
> lset ireader 1 javascript
> lrange ireader 0 -1
2) "javascript"
插入元素 使用linsert指令在清單的中間位置插入元素,有經驗的程式員都知道在插入元素時,我們經常搞不清楚是在指定位置的前面插入還是後面插入,是以antirez在linsert指令裡增加了方向參數before/after來顯示訓示前置和後置插入。不過讓人意想不到的是linsert指令并不是通過指定位置來插入,而是通過指定具體的值。這是因為在分布式環境下,清單的元素總是頻繁變動的,意味着上一時刻計算的元素下标在下一時刻可能就不是你所期望的下标了。
> linsert ireader before java ruby
(integer) 4
2) "ruby"
3) "java"
4) "python"
到目前位置,我還沒有在實際應用中發現插入指定的應用場景。
删除元素 清單的删除操作也不是通過指定下标來确定元素的,你需要指定删除的最大個數以及元素的值
> lrem ireader 1 java
定長清單 在實際應用場景中,我們有時候會遇到「定長清單」的需求。比如要以走馬燈的形式實時顯示中獎使用者名清單,因為中獎使用者實在太多,能顯示的數量一般不超過100條,那麼這裡就會使用到定長清單。維持定長清單的指令是ltrim,需要提供兩個參數start和end,表示需要保留清單的下标範圍,範圍之外的所有元素都将被移除。
> rpush ireader go java python javascript ruby erlang rust cpp
(integer) 8
> ltrim ireader -3 -1
1) "erlang"
2) "rust"
3) "cpp"
如果指定參數的end對應的真實下标小于start,其效果等價于del指令,因為這樣的參數表示需要需要保留清單元素的下标範圍為空。
hash
哈希等價于Java語言的HashMap或者是Python語言的dict,在實作結構上它使用二維結構,第一維是數組,第二維是連結清單,hash的内容key和value存放在連結清單中,數組裡存放的是連結清單的頭指針。通過key查找元素時,先計算key的hashcode,然後用hashcode對數組的長度進行取模定位到連結清單的表頭,再對連結清單進行周遊擷取到相應的value值,連結清單的作用就是用來将産生了「hash碰撞」的元素串起來。Java語言開發者會感到非常熟悉,因為這樣的結構和HashMap是沒有差別的。哈希的第一維數組的長度也是2^n。
增加元素 可以使用hset一次增加一個鍵值對,也可以使用hmset一次增加多個鍵值對
> hset ireader go fast
> hmset ireader java fast python slow
擷取元素 可以通過hget定位具體key對應的value,可以通過hmget擷取多個key對應的value,可以使用hgetall擷取所有的鍵值對,可以使用hkeys和hvals分别擷取所有的key清單和value清單。這些操作和Java語言的Map接口是類似的。
> hmset ireader go fast java fast python slow
> hget ireader go
"fast"
> hmget ireader go python
1) "fast"
2) "slow"
> hgetall ireader
2) "fast"
4) "fast"
5) "python"
6) "slow"
> hkeys ireader
> hvals ireader
3) "slow"
删除元素 可以使用hdel删除指定key,hdel支援同時删除多個key
> hdel ireader go
> hdel ireader java python
(integer) 2
判斷元素是否存在 通常我們使用hget獲得key對應的value是否為空就直到對應的元素是否存在了,不過如果value的字元串長度特别大,通過這種方式來判斷元素存在與否就略顯浪費,這時可以使用hexists指令。
> hexists ireader go
計數器 hash結構還可以當成計數器來使用,對于内部的每一個key都可以作為獨立的計數器。如果value值不是整數,調用hincrby指令會出錯。
> hincrby ireader go 1
> hincrby ireader python 4
> hincrby ireader java 4
2) "1"
4) "4"
5) "java"
6) "4"
> hset ireader rust good
127.0.0.1:6379> hincrby ireader rust 1
(error) ERR hash value is not an integer
擴容 當hash内部的元素比較擁擠時(hash碰撞比較頻繁),就需要進行擴容。擴容需要申請新的兩倍大小的數組,然後将所有的鍵值對重新配置設定到新的數組下标對應的連結清單中(rehash)。如果hash結構很大,比如有上百萬個鍵值對,那麼一次完整rehash的過程就會耗時很長。這對于單線程的Redis裡來說有點壓力山大。是以Redis采用了漸進式rehash的方案。它會同時保留兩個新舊hash結構,在後續的定時任務以及hash結構的讀寫指令中将舊結構的元素逐漸遷移到新的結構中。這樣就可以避免因擴容導緻的線程卡頓現象。
縮容 Redis的hash結構不但有擴容還有縮容,從這一點出發,它要比Java的HashMap要厲害一些,Java的HashMap隻有擴容。縮容的原理和擴容是一緻的,隻不過新的數組大小要比舊數組小一倍。
set
Java程式員都知道HashSet的内部實作使用的是HashMap,隻不過所有的value都指向同一個對象。Redis的set結構也是一樣,它的内部也使用hash結構,所有的value都指向同一個内部值。
增加元素 可以一次增加多個元素
> sadd ireader go java python
讀取元素 使用smembers列出所有元素,使用scard擷取集合長度,使用srandmember擷取随機count個元素,如果不提供count參數,預設為1
> smembers ireader
1) "java"
2) "python"
3) "go"
> scard ireader
> srandmember ireader
删除元素 使用srem删除一到多個元素,使用spop删除随機一個元素
> sadd ireader go java python rust erlang
(integer) 5
> srem ireader go java
> spop ireader
"erlang"
判斷元素是否存在 使用sismember指令,隻能接收單個元素
> sismember ireader rust
> sismember ireader javascript
(integer) 0
sortedset
SortedSet(zset)是Redis提供的一個非常特别的資料結構,一方面它等價于Java的資料結構Map<String, Double>,可以給每一個元素value賦予一個權重score,另一方面它又類似于TreeSet,内部的元素會按照權重score進行排序,可以得到每個元素的名次,還可以通過score的範圍來擷取元素的清單。
zset底層實作使用了兩個資料結構,第一個是hash,第二個是跳躍清單,hash的作用就是關聯元素value和權重score,保障元素value的唯一性,可以通過元素value找到相應的score值。跳躍清單的目的在于給元素value排序,根據score的範圍擷取元素清單。
增加元素 通過zadd指令可以增加一到多個value/score對,score放在前面
> zadd ireader 4.0 python
> zadd ireader 4.0 java 1.0 go
長度 通過指令zcard可以得到zset的元素個數
> zcard ireader
删除元素 通過指令zrem可以删除zset中的元素,可以一次删除多個
> zrem ireader go python
計數器 同hash結構一樣,zset也可以作為計數器使用。
> zadd ireader 4.0 python 4.0 java 1.0 go
> zincrby ireader 1.0 python
"5"
擷取排名和分數 通過zscore指令擷取指定元素的權重,通過zrank指令擷取指定元素的正向排名,通過zrevrank指令擷取指定元素的反向排名[倒數第一名]。正向是由小到大,負向是由大到小。
> zscore ireader python
> zrank ireader go # 分數低的排名考前,rank值小
> zrank ireader java
> zrank ireader python
> zrevrank ireader python
根據排名範圍擷取元素清單 通過zrange指令指定排名範圍參數擷取對應的元素清單,攜帶withscores參數可以一并擷取元素的權重。通過zrevrange指令按負向排名擷取元素清單[倒數]。正向是由小到大,負向是由大到小。
> zrange ireader 0 -1 # 擷取所有元素
127.0.0.1:6379> zrange ireader 0 -1 withscores
6) "5"
> zrevrange ireader 0 -1 withscores
1) "python"
2) "5"
5) "go"
6) "1"
根據score範圍擷取清單 通過zrangebyscore指令指定score範圍擷取對應的元素清單。通過zrevrangebyscore指令擷取倒排元素清單。正向是由小到大,負向是由大到小。參數-inf表示負無窮,+inf表示正無窮。
> zrangebyscore ireader 0 5
> zrangebyscore ireader -inf +inf withscores
> zrevrangebyscore ireader +inf -inf withscores # 注意正負反過來了
根據範圍移除元素清單 可以通過排名範圍,也可以通過score範圍來一次性移除多個元素
> zremrangebyrank ireader 0 1
(integer) 2 # 删掉了2個元素
> zremrangebyscore ireader -inf 4
> zrange ireader 0 -1
跳躍清單 zset内部的排序功能是通過「跳躍清單」資料結構來實作的,它的結構非常特殊,也比較複雜。這一塊的内容深度讀者要有心理準備。
因為zset要支援随機的插入和删除,是以它不好使用數組來表示。我們先看一個普通的連結清單結構。
我們需要這個連結清單按照score值進行排序。這意味着當有新元素需要插入時,需要定位到特定位置的插入點,這樣才可以繼續保證連結清單是有序的。通常我們會通過二分查找來找到插入點,但是二分查找的對象必須是數組,隻有數組才可以支援快速位置定位,連結清單做不到,那該怎麼辦?
想想一個創業公司,剛開始隻有幾個人,團隊成員之間人人平等,都是聯合創始人。随着公司的成長,人數漸漸變多,團隊溝通成本随之增加。這時候就會引入組長制,對團隊進行劃分。每個團隊會有一個組長。開會的時候分團隊進行,多個組長之間還會有自己的會議安排。公司規模進一步擴充,需要再增加一個層級——部門,每個部門會從組長清單中推選出一個代表來作為部長。部長們之間還會有自己的高層會議安排。
跳躍清單就是類似于這種層級制,最下面一層所有的元素都會串起來。然後每隔幾個元素挑選出一個代表來,再将這幾個代表使用另外一級指針串起來。然後在這些代表裡再挑出二級代表,再串起來。最終就形成了金字塔結構。
想想你老家在世界地圖中的位置:亞洲-->中國->安徽省->安慶市->枞陽縣->湯溝鎮->田間村->xxxx号,也是這樣一個類似的結構。
「跳躍清單」之是以「跳躍」,是因為内部的元素可能「身兼數職」,比如上圖中間的這個元素,同時處于L0、L1和L2層,可以快速在不同層次之間進行「跳躍」。
定位插入點時,先在頂層進行定位,然後下潛到下一級定位,一直下潛到最底層找到合适的位置,将新元素插進去。你也許會問那新插入的元素如何才有機會「身兼數職」呢?
跳躍清單采取一個随機政策來決定新元素可以兼職到第幾層,首先L0層肯定是100%了,L1層隻有50%的機率,L2層隻有25%的機率,L3層隻有12.5%的機率,一直随機到最頂層L31層。絕大多數元素都過不了幾層,隻有極少數元素可以深入到頂層。清單中的元素越多,能夠深入的層次就越深,能進入到頂層的機率就會越大。
這還挺公平的,能不能進入中央不是靠拼爹,而是看運氣。
我有一個微信公衆号,經常會分享一些Java技術相關的幹貨;如果你喜歡我的分享,可以用微信搜尋“Java團長”或者“javatuanzhang”關注。
---------------------
作者:Java程式員-張凱
來源:CSDN
原文:https://blog.csdn.net/qq_41701956/article/details/81198804
版權聲明:本文為部落客原創文章,轉載請附上博文連結!