您好,登錄后才能下訂單哦!
小編給大家分享一下Redis基礎數據結構的用法示例,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
1. 數據類型
其基礎數據類型有String、List、Hash、Set、Sorted Set,這些都是常用的基礎數據類型,可以看到非常豐富,幾乎能夠滿足大部分的需求了。其實還有一些高級數據結構,我們在這章里暫時先不提,只聊基礎的數據結構。
2. String
String可以說是最基礎的數據結構了, 用法上可以直接和Java中的String掛鉤,你可以把String類型用于存儲某個標志位,某個計數器,甚至狠一點,序列化之后的JSON字符串都行,其單個key限制為512M。其常見的命令為get、set、incr、decr 、mget。
2.1 使用
get 獲取某個key,如果key不存在會返回空指針
set 給key賦值,將key設置為指定的值,如果該key之前已經有值了,那么將被新的值給覆蓋
incr 給當前的key的值+1,如果key不存在則會先給key調用set賦值為0,再調用incr。當然如果該key的類型不能做加法運算,例如字符串,就會拋出錯誤
decr 給當前key的值-1,其余的同上
mget 同get,只是一次性返回多條數據,不存在的key將會返回空指針
string相關命令
可能大多數的人只是到用一用的地步,這也無可厚非,但是如果是作為一個對技術有追求的開發,或者說你有想近大廠的想法,一定要有刨根問底的精神。只有當你真正知道一個東西的底層原理時,你遇到問題時才能提供給你更多的思路去解決問題。接下來我們就來聊一下Redis中String底層是如何實現的。
2.2 原理
2.2.1 結構
我們知道Redis是用C語言寫的,但是Redis卻沒有直接使用,而是自己實現了一個叫SDS(Simple Dynamic String)的結構來實現字符串,結構如下。
struct sdshdr { // 記錄buf中已使用的字節數量 int len; // 記錄buf中未使用的字節數量 int free; // 字節數組,用于保存字符串 char buf[]; }
2.2.2 優點
為什么Redis要自己實現SDS而不是直接用C的字符串呢?主要是因為以下幾點。
減少獲取字符串長度開銷 C語言中獲取字符串的長度需要遍歷整個字符串,直到遇到結束標志位\0,時間復雜度為O(n),而SDS直接維護了長度的變量,取長度的時間復雜度為O(1)
避免緩沖區溢出 C語言中如果往一個字節數組中塞入超過其容量的字節,那么就會造成緩沖區溢出,而SDS通過維護free變量解決了這個問題。向buf數組中寫入數據時,會先判斷剩余的空間是否足夠塞入新數據,如果不夠,SDS就會重新分配緩沖區,加大之前的緩沖區。且加大的長度等于新增的數據的長度
空間預分配&空間惰性釋放 C語言中,每次修改字符串都會重新分配內存空間,如果對字符串修改了n次,那么必然會出現n次內存重新分配。而SDS由于冗余了一部分空間,優化了這個問題,將必然重新分配n次變為最多分配n次,而數據從buf中移除的時候,空閑出來的內存也不會馬上被回收,防止新寫入數據而造成內存重新分配
保證二進制安全 C語言中,字符串遇到\0會被截斷,而SDS不會因為數據中出現了\0而截斷字符串,換句話說,不會因為一些特殊的字符影響實際的運算結果
可以結合下面的圖來理解SDS。
圖片來源于網絡,侵刪
總結一下就是上面列表的四個小標題,為了減少獲取字符串長度開銷、避免緩沖區溢出、空間預分配&空間惰性釋放和保證二進制安全。
3. List
List也是一個使用頻率很高的數據結構,其涉及到的命令太多了,就不像String那樣一個一個演示了,感興趣的大家可以去搜一搜。命令有lpush、lpushx、rpush、rpushx、lpop、rpop、lindex、linsert、lrange、llen、lrem、lset、ltrim、rpoplpush、brpoplpush、blpop、brpop,其都是對數組中的元素的操作。
3.1 使用
List的用途我認為主要集中在以下兩個方面。
當作普通列表存儲數據(類似于Java的ArrayList)
用做異步隊列
普通列表這個自然不必多說,其中存放的必然業務中需要的數據,下面來著重聊一下異步隊列。
啥玩意,List還能當成隊列來玩?
List除了能被用做隊列,還能當作棧來使用。在上面介紹了很多操作List命令,當我們用rpush/lpop組合命令的時候,實際上就是在使用一個隊列,而當我們用rpush/rpop命令組合的時候,就是在使用一個棧。lpush/rpop和lpush/lpop是同理的。
假設我們用的是rpush來生產消息,當我們的程序需要消費消息的時候,就使用lpop從異步隊列中消費消息。但是如果采用這種方式,當隊列為空時,你可能需要不停的去詢問隊列中是否有數據,這樣會造成機器的CPU資源的浪費。
所以你可以采取讓當前線程Sleep一段時間,這樣的確可以節省一部分CPU資源。但是你可能就需要去考慮Sleep的時間,間隔太短,CPU上下文切換可能也是一筆不小的開銷,間隔太長,那么可能造成這條消息被延遲消費(不過都用異步隊列了,應該可以忽略這個問題)。
除了Sleep,還有沒有其他的方式?
有,答案是blpop。當我們使用blpop去消費時,如果當前隊列是空的,那么此時線程會阻塞住,直到下面兩種condition。
達到設置的timeout時間
隊列中有消息可以被消費
比起Sleep一段時間,實時性會好一點;比起輪詢,對CPU資源更加友好。
3.2 原理
在Redis3.2之前,Redis采用的是ZipList(壓縮列表)或者LinkedList(鏈表)。當List中的元素同時滿足每個元素的小于64字節和List元素個數小于512個時,存儲的方式為ZipList。但凡有一個條件沒滿足就會轉換為LinkedList。
而在3.2之后,其實現變成了QuickList(快速列表)。LinkedList由于是較為基礎的東西,此處就不贅述了。
3.2.1 ZipList
ZipList采用連續的內存緊湊存儲,不像鏈表那樣需要有額外的空間來存儲前驅節點和后續節點的指針。按照其存儲的區域劃分,大致可以分為三個部分,每個部分也有自己的劃分,其詳細的結構如下。
header ziplist的頭部信息
zlbytes 標識ziplist所占用的內存字節數
zltail 到ziplist尾節點的偏移量
zllen ziplist中的存儲的節點數量
entries 存儲實際節點的信息
pre_entry_length 記錄了前一個節點的長度,通過這個值可以快速的跳轉到上一個節點
encoding 顧名思義,存儲量元素的編碼格式
length 所存儲數據的長度
content 保存節點的內容
end 標識ziplist的末尾
如果采用鏈表的存儲方式,鏈表中的元素由指針相連,這樣的方式可能會造成一定的內存碎片。而指針也會占用額外的存儲空間。而ZipList不會存在這些情況,ZipList占用的是一段連續的內存空間。
但是相應地,ZipList的修改操作效率較為低下,插入和刪除的操作會設計到頻繁的內存空間申請和釋放(有點類似于ArrayList重新擴容),且查詢效率同樣會受影響,本質上ZipList查詢元素就是遍歷鏈表。
3.2.2 QuickList
在3.2版本之后,list的實現就換成了QuickList。QuickList將list分成了多個節點,每一個節點采用ZipList存儲數據。
4. Hash
其用法就跟Java中的HashMap中一樣,都是往map中去丟鍵值對。
4.1 使用
基礎的命令如下:
hset 在hash中設置鍵值對
hget 獲hash中的某個key值
hdel 刪除hash中某個鍵
hlen 統計hash中元素的個數
hmget 批量的獲取hash中的鍵的值
hmset 批量的設置hash中的鍵和值
hexists 判斷hash中某個key是否存在
hkeys 返回hash中的所有鍵(不包含值)
hvals 返回hash中的所有值(不包含鍵)
hgetall 獲取所有的鍵值對,包含了鍵和值
其實大多數情況下的使用跟HashMap是差不多的,沒有什么較為特殊的地方。
4.2 原理
hash的底層實現也是有兩種,ZipList和HashTable。但具體采用哪一種與Redis的版本無關,而與當前hash中所存的元素有關。首先當我們創建一個hash的時候,采用的ZipList進行存儲。隨著hash中的元素增多,達到了Redis設定的閾值,就會轉換為HashTable。
其設定的閾值如下:
存儲的某個鍵或者值長度大于默認值(64)
ZipList中存儲的元素數量大于默認值(512)
ZipList上面我們專門簡單分析了一下,理解這個設定應該也比較容易。當ZipList中的元素過多的時候,其查詢的效率就會變得低下。而HashTable的底層設計其實和Java中的HashMap差不多,都是通過拉鏈法解決哈希沖突。具體的可以參考從基礎的使用來深挖HashMap這篇文章。
5. Set
Set的概念可以與Java中的Set劃等號,用于存儲一個不包含重復元素的集合。
5.1 使用
其主要的命令如下,key代表redis中的Set,member代表集合中的元素。
sadd sadd key member [...] 將一個或者多個元素加入到集合中,如果有已經存在的元素會忽略掉。
srem srem key member [...]將一個或者多個元素從集合中移除,不存在的元素會被忽略掉
smembers smembers key返回集合中的所有成員
sismember dismember key member判斷member在key中是否存在,如果存在則返回1,如果不存在則返回0
scard scard key返回集合key中的元素的數量
smove move source destination member將元素從source集合移動到destination集合。如果source中不包含member,則不會執行任何操作,當且僅當存在才會從集合中移出。如果destination已經存在元素則不會對destination做任何操作。該命令是原子操作。
spop spop key隨機刪除并返回集合中的一個元素
srandmember srandmember key與spop一樣,只不過不會將元素刪除,可以理解為從集合中隨機出一個元素來。
sinter 求一個或者多個集合的交集
sinterstore sinterstore destination key [...]與sinter類似,但是會將得出的結果存到destination中。
sunion 求一個或者多個集合的并集
sunionstore sunionstore destination key [...]
sdiff 求一個或者多個集合的差集
sdiffstore sdiffstore destination key [...]與sdiff類似,但是會將得出的結果存到destination中。
5.2 原理
我們知道Java中的Set有多種實現。在Redis中也是,有IntSet和HashTable兩種實現,首先初始化的時候使用的是IntSet,而滿足如下的條件時,就會轉換成HashTable。
當集合中保存的所有元素都是整數時
集合對象保存的元素數量不超過512
上面已經簡單的介紹了HashTable了,所以這里只聊聊IntSet。
5.2.1 IntSet
intset底層是一個數組,既然數據結構是數組,那么存儲數據就可以是有序的,這也使得intset的底層查詢是通過二分查找來實現。其結構如下。
struct intset { // 編碼方式 uint32_t encoding; // 集合包含元素的數量 uint32_t length; // 存儲元素的數組 int8_t contents[]; }
與ZipList類似,IntSet也是使用的一連串的內存空間,但是不同的是ZipList可以存儲二進制的內容,而IntSet只能存儲整數;且ZipList存儲是無序的,IntSet則是有序的,這樣一來,元素個數相同的前提下,IntSet的查詢效率會更高。
6. Sorted
Set其與Set的功能大致類似,只不過在此基礎上,可以給每一個元素賦予一個權重。你可以理解為Java的TreeSet。與List、Hash、Set一樣,其底層的實現也有兩種,分別是ZipList和SkipList(跳表)。
初始化Sorted Set的時候,會采用ZipList作為其實現,其實很好理解,這個時候元素的數量很少,采用ZipList進行緊湊的存儲會更加的節省空間。當期達到如下的條件時,就會轉換為SkipList:
其保存的元素數量的個數小于128個
其保存的所有元素長度小于64字節
6.1 使用
下面的命令中,key代表zset的名字;4代表score,也就是權重;而member就是zset中的key的名稱。
zadd zadd key 4 member用于增加元素
zcard zcard key用于獲取zset中的元素的數量
zrem zrem key member [...]刪除zset中一個或者多個key
zincrby zincrby key 1 member給key的權重值加上score的值(也就是1)
zscore zscore key member用于獲取指定key的權重值
zrange zrange key 0 -1獲取zset中所有的元素,zrange key 0 -1 withscores獲取所有元素和權重,withscores參數的作用是決定是否將權重值也一起返回。其返回的元素按照從小到大排序,如果元素具有相同的權重,則會按照字典序排序。
zrevrange zrevrange key 0 -1 withscores,其與zrange類似,只不過zrevrange按照從大到小排序。
zrangebyscore zrangebyscore key 1 5,返回key中權重在區間(1, 5]范圍內元素。當然也可以使用withscores來將權重值一并返回。其元素按照從小到大排序。1代表min,5代表max,他們也可以分別是**-inf和inf**,當你不知道key中的score區間時,就可以使用這個。還有一個類似于SQL中的limit的可選參數,在此就不贅述。
除了能夠對其中的元素添加權重之外,使用ZSet還可以實現延遲隊列。
延遲隊列用于存放延遲任務,那什么是延遲隊列呢?
舉個很簡單的例子, 你在某個電商APP中下訂單,但是沒有付款,此時它會提醒你,「訂單如果超過1個小時沒有支付,將會自動關閉」;再比如在某個活動結束前1個小時給用戶推送消息;再比如訂單完成后多少天自動確認收貨等等。
用人話解釋一遍,那就是過會才要干的事情。
那ZSet怎么實現這個功能?
其實很簡單,就是將任務的執行時間設置為ZSet中的元素權重,然后通過一個后臺線程定時的從ZSet中查詢出權重最小的元素,然后通過與當前時間戳判斷,如果大于當前時間戳(也就是該執行了)就將其從ZSet中取出。
那,怎么取?
其實我看很多講Redis實現延遲隊列的博客都沒有把這個怎么取講清楚,到底該用什么命令,傳什么參數。我們使用zrangebyscore命令來實現,還記得-inf和inf嗎,其全稱是infinity,分別表示無限小和無限大。
由于我們并不知道延遲隊列當中的score(也就是任務執行時間)的范圍,所以我們可以直接使用-inf和inf,完整命令如下。
zrangescore key -inf inf limt 0 1 withscores
還是有點用,那ZSet底層是咋實現的呢?
上面已經講過了ZipList,就不贅述,下面聊聊SkipList。
6.2 原理
6.2.1 SkipList
SkipList存在于zset(Sorted Set)的結構中,如下:
struct zset { // 字典 dict *dict; // 跳表 zskiplist *zsl; }
而SkipList的結構如下:
struct zskiplist { // 表頭節點和表尾節點 struct zskiplistNode *header, *tail; // 表中節點的數量 unsigned long length; // 表中層數最大的節點的層數 int level; }
不知道大家是否有想過,為什么Redis要使用SkipList來實現ZSet,而不用數組呢?
首先ZSet如果數組存儲的話,由于ZSet中存儲的元素是有序的,存入的時候需要將元素放入數組中對應的位置。這樣就會對數組進行頻繁的增刪,而頻繁的增刪在數組中效率并不高,因為涉及到數組元素的移動,如果元素插入的位置是首位,那么后面的所有元素都要被移動。
所以為了應付頻繁增刪的場景,我們需要使用到鏈表。但是隨著鏈表的元素增多,同樣的會出現問題,雖然增刪的效率提升了,但是查詢的效率變低了,因為查詢元素會從頭到尾的遍歷鏈表。所有如果有什么方法能夠提升鏈表的查詢效率就好了。
于是跳表就誕生了。基于單鏈表,從第一個節點開始,每隔一個節點,建立索引。其實也是單鏈表。只不是中間省略了節點。
例如存在個單鏈表 1 3 4 5 7 8 9 10 13 16 17 18
抽象之后的索引為 1 4 7 9 13 17
如果要查詢16只需要在索引層遍歷到13,然后根據13存儲的下層節點(真實鏈表節點的地址),此時只需要再遍歷兩個節點就可以找到值為16的節點。
所以可以重新給跳表下一個定義,鏈表加多級索引的結構,就是跳表
在跳表中,查詢任意數據的時間復雜度是O(logn)。時間復雜度跟二分查找是一樣的。可以換句話說,用單鏈表實現了二分查找。但這也是一種用空間換時間的思路,并不是免費的。
以上是“Redis基礎數據結構的用法示例”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。