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 # 返回長度 > get ireader "beijing.zhangyue.keji.gufen.wooxian.gongsi"
追加子串
> append ireader .hao (integer) 46 # 返回長度 > get ireader "beijing.zhangyue.keji.gufen.wooxian.gongsi.hao"
遺憾的是字串沒有提供字串插入方法和子串刪除方法。
計數器 如果字串的內容是一個整數,那麼還可以將字串當成計數器來使用。
> set ireader 42 OK > get ireader "42" > incrby ireader 100 (integer) 142 > get ireader "142" > decrby ireader 100 (integer) 42 > get ireader "42" > incr ireader # 等價於incrby ireader 1 (integer) 43 > decr ireader # 等價於decrby ireader 1 (integer) 42
計數器是有範圍的,它不能超過Long.Max,不能低於Long.MIN
> set ireader 9223372036854775807 OK > incr ireader (error) ERR increment or decrement would overflow > set ireader -9223372036854775808 OK > decr ireader (error) ERR increment or decrement would overflow
過期和刪除 字串可以使用del指令進行主動刪除,可以使用expire指令設定過期時間,到點會自動刪除,這屬於被動刪除。可以使用ttl指令獲取字串的壽命。
> expire ireader 60 (integer) 1 # 1表示設定成功,0表示變數ireader不存在 > ttl ireader (integer) 50 # 還有50秒的壽命,返回-2表示變數不存在,-1表示沒有設定過期時間 > del ireader (integer) 1 # 刪除成功返回1 > get ireader (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" > lpop ireader "java" > lpop ireader "python" # 左進右出 > lpush ireader go java python (integer) 3 > rpop ireader "go" ... # 右進右出 > rpush ireader go java python (integer) 3 > rpop ireader "python" ... # 左進左出 > lpush ireader go java python (integer) 3 > lpop ireader "python" ...
在日常應用中,列表常用來作為非同步佇列來使用。
長度 使用llen指令獲取連結串列長度
> rpush ireader go java python (integer) 3 > llen ireader (integer) 3
隨機讀 可以使用lindex指令訪問指定位置的元素,使用lrange指令來獲取連結串列子元素列表,提供start和end下標引數
> rpush ireader go java python (integer) 3 > lindex ireader 1 "java" > lrange ireader 0 2 1) "go" 2) "java" 3) "python" > lrange ireader 0 -1 # -1表示倒數第一 1) "go" 2) "java" 3) "python"
使用lrange獲取全部元素時,需要提供end_index,如果沒有負下標,就需要首先通過llen指令獲取長度,才可以得出end_index的值,有了負下標,使用-1代替end_index就可以達到相同的效果。
修改元素 使用lset指令在指定位置修改元素。
> rpush ireader go java python (integer) 3 > lset ireader 1 javascript OK > lrange ireader 0 -1 1) "go" 2) "javascript" 3) "python"
> rpush ireader go java python (integer) 3 > linsert ireader before java ruby (integer) 4 > lrange ireader 0 -1 1) "go" 2) "ruby" 3) "java" 4) "python"
到目前位置,我還沒有在實際應用中發現插入指定的應用場景。
刪除元素 列表的刪除操作也不是通過指定下標來確定元素的,你需要指定刪除的最大個數以及元素的值
> rpush ireader go java python (integer) 3 > lrem ireader 1 java (integer) 1 > lrange ireader 0 -1 1) "go" 2) "python"
> rpush ireader go java python javascript ruby erlang rust cpp (integer) 8 > ltrim ireader -3 -1 OK > lrange ireader 0 -1 1) "erlang" 2) "rust" 3) "cpp"
如果指定引數的end對應的真實下標小於start,其效果等價於del指令,因為這樣的參數列示需要需要保留列表元素的下標範圍為空。
快速列表
hash
增加元素 可以使用hset一次增加一個鍵值對,也可以使用hmset一次增加多個鍵值對
> hset ireader go fast (integer) 1 > hmset ireader java fast python slow OK
> hmset ireader go fast java fast python slow OK > hget ireader go "fast" > hmget ireader go python 1) "fast" 2) "slow" > hgetall ireader 1) "go" 2) "fast" 3) "java" 4) "fast" 5) "python" 6) "slow" > hkeys ireader 1) "go" 2) "java" 3) "python" > hvals ireader 1) "fast" 2) "fast" 3) "slow"
刪除元素 可以使用hdel刪除指定key,hdel支援同時刪除多個key
> hmset ireader go fast java fast python slow OK > hdel ireader go (integer) 1 > hdel ireader java python (integer) 2
判斷元素是否存在 通常我們使用hget獲得key對應的value是否為空就直到對應的元素是否存在了,不過如果value的字串長度特別大,通過這種方式來判斷元素存在與否就略顯浪費,這時可以使用hexists指令。
> hmset ireader go fast java fast python slow OK > hexists ireader go (integer) 1
計數器 hash結構還可以當成計數器來使用,對於內部的每一個key都可以作為獨立的計數器。如果value值不是整數,呼叫hincrby指令會出錯。
> hincrby ireader go 1 (integer) 1 > hincrby ireader python 4 (integer) 4 > hincrby ireader java 4 (integer) 4 > hgetall ireader 1) "go" 2) "1" 3) "python" 4) "4" 5) "java" 6) "4" > hset ireader rust good (integer) 1 > 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 (integer) 3
讀取元素 使用smembers列出所有元素,使用scard獲取集合長度,使用srandmember獲取隨機count個元素,如果不提供count引數,預設為1
> sadd ireader go java python (integer) 3 > smembers ireader 1) "java" 2) "python" 3) "go" > scard ireader (integer) 3 > srandmember ireader "java"
刪除元素 使用srem刪除一到多個元素,使用spop刪除隨機一個元素
> sadd ireader go java python rust erlang (integer) 5 > srem ireader go java (integer) 2 > spop ireader "erlang"
判斷元素是否存在 使用sismember指令,只能接收單個元素
> sadd ireader go java python rust erlang (integer) 5 > sismember ireader rust (integer) 1 > 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 (integer) 1 > zadd ireader 4.0 java 1.0 go (integer) 2
長度 通過指令zcard可以得到zset的元素個數
> zcard ireader (integer) 3
刪除元素 通過指令zrem可以刪除zset中的元素,可以一次刪除多個
> zrem ireader go python (integer) 2
> zadd ireader 4.0 python 4.0 java 1.0 go (integer) 3 > zincrby ireader 1.0 python "5"
獲取排名和分數 通過zscore指令獲取指定元素的權重,通過zrank指令獲取指定元素的正向排名,通過zrevrank指令獲取指定元素的反向排名[倒數第一名]。正向是由小到大,負向是由大到小。
> zscore ireader python "5" > zrank ireader go # 分數低的排名考前,rank值小 (integer) 0 > zrank ireader java (integer) 1 > zrank ireader python (integer) 2 > zrevrank ireader python (integer) 0
根據排名範圍獲取元素列表 通過zrange指令指定排名範圍引數獲取對應的元素列表,攜帶withscores引數可以一併獲取元素的權重。通過zrevrange指令按負向排名獲取元素列表[倒數]。正向是由小到大,負向是由大到小。
> zrange ireader 0 -1 # 獲取所有元素 1) "go" 2) "java" 3) "python" > zrange ireader 0 -1 withscores 1) "go" 2) "1" 3) "java" 4) "4" 5) "python" 6) "5" > zrevrange ireader 0 -1 withscores 1) "python" 2) "5" 3) "java" 4) "4" 5) "go" 6) "1"
根據score範圍獲取列表 通過zrangebyscore指令指定score範圍獲取對應的元素列表。通過zrevrangebyscore指令獲取倒排元素列表。正向是由小到大,負向是由大到小。引數-inf
表示負無窮,+inf
表示正無窮。
> zrangebyscore ireader 0 5 1) "go" 2) "java" 3) "python" > zrangebyscore ireader -inf +inf withscores 1) "go" 2) "1" 3) "java" 4) "4" 5) "python" 6) "5" > zrevrangebyscore ireader +inf -inf withscores # 注意正負反過來了 1) "python" 2) "5" 3) "java" 4) "4" 5) "go" 6) "1"
根據範圍移除元素列表 可以通過排名範圍,也可以通過score範圍來一次性移除多個元素
> zremrangebyrank ireader 0 1 (integer) 2 # 刪掉了2個元素 > zadd ireader 4.0 java 1.0 go (integer) 2 > zremrangebyscore ireader -inf 4 (integer) 2 > zrange ireader 0 -1 1) "python"
跳躍列表 zset內部的排序功能是通過「跳躍列表」資料結構來實現的,它的結構非常特殊,也比較複雜。這一塊的內容深度讀者要有心理準備。
因為zset要支援隨機的插入和刪除,所以它不好使用陣列來表示。我們先看一個普通的連結串列結構。
我們需要這個連結串列按照score值進行排序。這意味著當有新元素需要插入時,需要定位到特定位置的插入點,這樣才可以繼續保證連結串列是有序的。通常我們會通過二分查詢來找到插入點,但是二分查詢的物件必須是陣列,只有陣列才可以支援快速位置定位,連結串列做不到,那該怎麼辦?
想想一個創業公司,剛開始只有幾個人,團隊成員之間人人平等,都是聯合創始人。隨著公司的成長,人數漸漸變多,團隊溝通成本隨之增加。這時候就會引入組長制,對團隊進行劃分。每個團隊會有一個組長。開會的時候分團隊進行,多個組長之間還會有自己的會議安排。公司規模進一步擴充套件,需要再增加一個層級——部門,每個部門會從組長列表中推選出一個代表來作為部長。部長們之間還會有自己的高層會議安排。
跳躍列表就是類似於這種層級制,最下面一層所有的元素都會串起來。然後每隔幾個元素挑選出一個代表來,再將這幾個代表使用另外一級指標串起來。然後在這些代表裡再挑出二級代表,再串起來。最終就形成了金字塔結構。
想想你老家在世界地圖中的位置:亞洲-->中國->安徽省->安慶市->樅陽縣->湯溝鎮->田間村->xxxx號,也是這樣一個類似的結構。
「跳躍列表」之所以「跳躍」,是因為內部的元素可能「身兼數職」,比如上圖中間的這個元素,同時處於L0、L1和L2層,可以快速在不同層次之間進行「跳躍」。
定位插入點時,先在頂層進行定位,然後下潛到下一級定位,一直下潛到最底層找到合適的位置,將新元素插進去。你也許會問那新插入的元素如何才有機會「身兼數職」呢?
跳躍列表採取一個隨機策略來決定新元素可以兼職到第幾層,首先L0層肯定是100%了,L1層只有50%的概率,L2層只有25%的概率,L3層只有12.5%的概率,一直隨機到最頂層L31層。絕大多數元素都過不了幾層,只有極少數元素可以深入到頂層。列表中的元素越多,能夠深入的層次就越深,能進入到頂層的概率就會越大。
這還挺公平的,能不能進入中央不是靠拼爹,而是看運氣。