「Redis」Redis 基礎

不積跬步,無以至千里;不積小流,無以成江海。

前言

前一節對 Redis 和 Memcache 做了對比,我們知道了 Redis 的一些優良特性;

由於是 <strong>純內存 操作,Redis 的性能非常出色,來自官網統計每秒可以處理超過 10 萬次讀寫操作,是已知 <strong>性能最快 的KV DB;( 期待能有更優秀的軟件誕生 )

國內各大互聯網公司也都在使用Redis,也是後臺開發者繞不開的必備技能;由於內存的寶貴,所以只有合適的數據結構用在最正確的地方才能發揮最大作用。

下面分析下 Redis 的基本數據結構及適用場景。

正文

Redis 有 5 種基礎數據結構,分別為:<strong>string (字符串)、<strong>list (列表)、<strong>set (集合)、<strong>hash (哈希) 和 <strong>zset (有序集合)。

Tips:至於更高級的 BloomFilter、Geo、pub/sub、HyperLogLog 後面會單獨介紹

Redis 所有操作都是 <strong>原子性 的,要麼成功執行要麼失敗完全不執行。多個操作也 <strong>支持事務

,通過 MULTI 和 EXEC 把所有單個指令包起來。

Redis 所有的數據結構都是以 <strong>唯一 的 key 字符串作為名稱,然後通過這個唯一 key 值來獲取相應的 value 數據。不同類型的數據結構的差異就在於 value 的結構不一樣。

1. String

常用命令: get/set mget/mset incr/decr等

介紹: 字符串 string 是 Redis 最簡單的數據結構,也是我們日常用的最多的一種類型。

string 是二進制安全的。也就是說 Redis 的 string 可以包含任何數據。比如 jpg 圖片或者序列化的對象。

「Redis」Redis 基礎

Redis 的字符串是動 <strong>態字符串,是可以修改的字符串,內部結構採用 <strong>預分配冗餘空間 的方式來減少內存的頻繁分配,內部為當前字符串實際分配的空間 capacity 一般要高於實際字符串長度 len。當字符串長度小於 <strong>1M 時,擴容都是 <strong>加倍 現有的空間,如果超過 1M,擴容時一次只會多擴 1M 的空間。需要注意的是字符串最大長度為 <strong>512M。

字符串是由 <strong>多個字節 組成,每個字節又是由 8 個 <strong>bit 組成,如此便可以將一個字符串看成很多 bit 的組合,這便是 <strong>bitmap「位圖」數據結構,後續也會拎出來說。

使用場景: 一個常見的用途就是緩存用戶信息。我們將用戶信息結構體使用 JSON 序列化成字符串,然後將序列化後的字符串塞進 <strong>Redis 來緩存。同樣,取用戶信息會經過一次 <strong>反序列化 的過程。還有如:計數器、分佈式系統共享 session 等

2. List

常用命令: lpush/rpush/lpop/rpop/lrange等;

介紹: Redis list的實現為一個 <strong>雙向鏈表,這意味著 list 的插入和刪除操作非常快,時間複雜度為 <strong>O(1),但是索引定位很慢,時間複雜度為 O(n),這點讓人非常意外。


「Redis」Redis 基礎

再深入一點,你會發現 Redis 底層存儲的還不是一個簡單的 <strong>linkedlist,而是稱之為快速鏈表 <strong>quicklist 的一個結構。

首先在列表元素較少的情況下會使用一塊連續的內存存儲,這個結構是 <strong>ziplist,也即是<strong>壓縮列表。

它將所有的元素緊挨著一起存儲,分配的是一塊 <strong>連續的內存。當數據量比較多的時候才會改成 <strong>quicklist。因為普通的鏈表需要的附加指針空間太大,會比較浪費空間,而且會加重內存的 <strong>碎片化。比如這個列表裡存的只是 int 類型的數據,結構上還需要兩個額外的指針 prev 和 next。所以 Redis 將鏈表和 ziplist 結合起來組成了 <strong>quicklist。也就是將多個 ziplist 使用 <strong>雙向指針 串起來使用。這樣既滿足了快速的插入刪除性能,又不會出現太大的空間冗餘。不得不說Redis為了高性能,真的是把內存用到了<strong>極致。

當列表彈出了最後一個元素之後,該數據結構自動被刪除,內存被回收。

使用場景: 常用來做異步隊列使用。將需要延後處理的任務結構體序列化成字符串塞進 list,另一個線程從這個列表中輪詢數據進行處理。

3. Hash

常用命令: hget/hset/hgetall等

介紹: Redis 的字典相當於 Java 語言裡面的 HashMap,它是無序字典。

不同的是,Redis 的字典的值只能是字符串,另外它們 <strong>rehash 的方式不一樣,因為 Java 的 HashMap 在字典很大時,rehash 是個耗時的操作,需要一次性全部 rehash。Redis 為了高性能,不能堵塞服務,所以採用了 <strong>漸進式 rehash 策略。

「Redis」Redis 基礎

何為漸進式 rehash

就是把拷貝節點數據的過程平攤到後續的操作中,而不是一次性拷貝。


所謂平攤到後續的操作中,就是對節點操作,例如再次插入,查找,刪除,修改時都會進行拷貝。


要想實現這個過程,一個hash結構必須要有以下字段:


兩個hash表。一個表拷貝到另一個表的容器。一個標識 rehashidx 來標明是否在進行 rehash 中。如果是,那麼對節點的操作啟動 rehash 過程。

何時啟動 rehash

每次插入鍵值對時,都會檢查是否需要擴容。當hash結構的第一個hash表 ht[0] 達到擴容條件就可以啟動了。此時重新調整並分配新的空間,將hash結構的第二個hash表 ht[1] 指向這個空間。

rehash 具體過程

1,為 ht[1] 分配空間, 讓 hash 同時持有 ht[0] 和 ht[1] 兩個哈希表。


2,在字典中維持一個索引計數器變量 rehashidx, 並將它的值設置為0, 表示 rehash 工作正式開始。


3,在 rehash 進行期間, 每次對 hash 執行添加、刪除、查找或者更新操作時, 程序除了執行指定的操作以外, 還會順帶將 ht[0] 哈希表在 rehashidx 索引上的所有鍵值對 rehash 到 ht[1], 當 rehash 工作完成之後, 程序將 rehashidx 屬性的值增 1。


4,隨著字典操作的不斷執行, 最終在某個時間點上, ht[0] 的所有鍵值對都會被 rehash 至 ht[1], 這時程序將 rehashidx 屬性的值設為 -1 , 表示 rehash 操作已完成。

漸進式 <strong>rehash 的好處在於它採取<strong>分而治之的方式, 將 rehash 鍵值對所需的計算工作均灘到對字典的每個添加、刪除、查找和更新操作上, 從而避免了集中式 rehash 而帶來的龐大計算量。

當全部節點搬移完成之後需要做什麼呢?

由於只有兩個 hash 表容器(也只需要兩個 hash 表容器就夠了)


如果 ht[1] 需要 rehash 時再搬移到 ht[0] 嗎?


當然這樣也是沒有問題的,但是顯得有點混亂,因為搞不清楚哪個容器是要搬移的。


巧妙的做法是搬移完成之後,讓 ht[0] 指向新的hash容器。這樣 ht[0] 永遠是那個要被搬移的對象,ht[1]是搬移過程中的中轉。

<strong>思路比結論更重要

當 hash 移除了最後一個元素之後,該數據結構自動被刪除,內存被回收。

使用場景: 我們要存儲一個用戶信息對象數據,其中包括用戶ID、用戶姓名、年齡和生日,通過用戶ID我們希望獲取該用戶的姓名或者年齡或者生日;

hash 結構存儲,不同於字符串一次性需要全部序列化整個對象,hash 可以對用戶結構中的每個字段單獨存儲。這樣當我們需要獲取用戶信息時可以進行部分獲取。而以整個字符串的形式去保存用戶信息的話就只能一次性全部讀取,這樣就會比較浪費網絡流量。

hash 也有缺點,hash 結構的存儲消耗要高於單個字符串,到底該使用 hash 還是字符串,需要根據實際情況再三權衡。

4. Set

常用命令: sadd/spop/smembers/sunion等;

介紹: Redis 的集合相當於 Java 語言裡面的 HashSet,它內部的鍵值對是 <strong>無序的唯一的。 它的內部實現相當於一個特殊的 hash,hash中所有的 value 都是一個值NULL。集合中最大的成員數為 2^32 - 1 (4294967295, 每個集合可存儲40多億個成員)。

當集合中最後一個元素移除之後,數據結構自動刪除,內存被回收。

使用場景: 存儲活動中獎的用戶 ID,因為有去重功能,可以保證同一個用戶不會中獎兩次。另外還可以用在有交集,並集的需求上。

5. Sorted Set

常用命令: zadd/zrange/zrem/zcard等;

介紹: zset 可能是 Redis 提供的最為特色的數據結構,它也是在面試中面試官最愛問的數據結構。它類似於 Java 的 SortedSet 和 HashMap 的結合體,一方面它是一個 set,保證了內部 value 的唯一性,另一方面它可以給每個 value 賦予一個 score,代表這個 value 的排序權重。

它的內部實現用的是一種叫做「<strong>跳躍表」的數據結構。使用跳躍表的結構可以獲得比較高的查找效率

使用場景: zset 可以用來存粉絲列表,value 值是粉絲的用戶 ID,score 是關注時間。我們可以對粉絲列表按關注時間進行排序。

zset 還可以用來存儲學生的成績,value 值是學生的 ID,score 是他的考試成績。我們可以對成績按分數進行排序就可以得到他的名次。

容器型數據結構的通用規則

list/set/hash/zset 這四種數據結構是容器型數據結構,它們共享下面兩條通用規則:

  • create if not exists 如果容器不存在,那就創建一個,再進行操作。比如 rpush 操作剛開始是沒有列表的,Redis 就會自動創建一個,然後再 rpush 進去新元素。
  • drop if no elements 如果容器裡元素沒有了,那麼立即刪除元素,釋放內存。這意味著 lpop 操作到最後一個元素,列表就消失了
點關注 不迷路

以上就是這篇的全部內容,能看到這裡的都是 <strong>人才。

如果你從本篇內容有收穫,求 <strong>點贊,求 <strong>關注,求 <strong>轉發 ,讓更多的人看到。


如果本文有任何錯誤,請批評指教,不勝感激 !


分享到:


相關文章: