吐血整理:Redis的基本數據類型,你懂多少?

前言

  以前項目有使用過redis作緩存,對redis的五種基本類型只是只知其一;不知其二,懂得如何去使用,可是沒有深刻探究。最近在讀老錢的《redis深度歷險:核心原理與應用實踐》這本書,以爲書上對redis的探索仍是比較深刻的,同窗們有興趣能夠去了解一下。話很少說,直接上乾貨。面試


Redis的基本數據類型

redis有5種基本的數據類型:redis

  • string(字符串)
  • list(有序列表)
  • hash(字典)
  • set(集合)
  • zset(有序集合)

一、string(字符串)

  • 數據結構:內部是一個字符數組,經過預分配的冗餘空間的方式減小內存的頻繁分配,相似 Java 的 ArrayList。
  • 擴容機制:當字符串小於1MB的時候,擴容是加倍現有的空間。當字符串長度超過1MB的時候,每次只會擴容1MB的空間,最大的長度爲512MB。
  • 應用場景
    1. 簡單的 key-value 緩存
    2. 做爲系統的實時計數器,支持 incr(自增)操做
    3. 共享用戶 session

二、list(有序列表)

  • 數據結構:它是一個雙向鏈表,相似於Java的LinkedList,這就意味着list的插入和刪除是很快的,可是查詢會比較慢。
  • 應用場景
    1. 消息隊列:使用左進右出的命令設計隊列,用於消息排隊和異步處理邏輯
    2. 棧:左進左出
    3. 列表分頁展現:當咱們的數據量愈來愈大的時候,每每須要分頁展現,該數據結構有序,而且支持範圍獲取元素,能夠完美地完成分頁查詢的功能。

深刻 list 的底層存儲結構算法

  • 列表元素在較少的狀況下,會使用一塊連續的內存存儲,全部元素彼此緊挨着一塊存儲,這個結構是 ziplist(壓縮列表)

  由於普通鏈表須要附加的指針空間太大,會形成空間浪費,也會加劇內存的碎片化。例如咱們的鏈表中存的是int型(4字節)的數據,若是用普通鏈表的話,結構上須要額外的 prev 和 next 指針(每一個指針佔8個字節)。形成沒必要要的浪費。 數組

  • 當數據量多的狀況下,會改成 quicklist(快速鏈表)

  快速鏈表是 ziplist(壓縮列表)和 linkedlist(雙端鏈表)的結合,也就是將多個 ziplist 使用雙向指針串起來使用。 緩存

探索「壓縮列表」的內部網絡

  • < zlbytes >: 32bit,表示 ziplist 佔用的字節總數(也包括< zlbytes >自己佔用的4個字節)。
  • < zltail_offset >: 32bit,表示ziplist表中最後一項(entry)在 ziplist 中的偏移字節數。(爲了快速定位最後一項的位置,在末尾進行新增或刪除)
  • < zllen >: 16bit,表示 ziplist 中數據項(entry)的個數。(能夠表達的最大值爲2^16-1,若是數據項個數超過了16bit能表達的最大值,ziplist仍然能夠來表示。若是< zllen >16bit全爲1的狀況,那麼< zllen >就不表示數據項個數了,這時候要想知道 ziplist 中數據項總數,那麼必須對 ziplist 從頭至尾遍歷各個數據項,才能計算出來。)
  • < entry >: 表示真正存放數據的數據項,長度不定。(內部有一個 prevlen 字段,表示前一個 entry 的字節長度,經過這個字段快速定位到上一個元素的起始位置,當字符串長度小於254時,使用1個字節表示;當超過254的時候,使用5個字節表示,第一個字節是0xfe,區分結束標記)
  • < zlend >: ziplist 最後1個字節,是一個結束標記,值固定等於255。

探索「快速鏈表」的內部session

  快速鏈表是 ziplist(壓縮列表)和 linkedlist(雙端鏈表)的結合,也就是將多個 ziplist 使用雙向指針串起來使用。 數據結構

爲何要這樣設計「快速鏈表」?
  總結起來,大概又是一個空間和時間的折中:
  > 雙向鏈表便於在表的兩端進行 push 和 pop 操做,可是它的內存開銷比較大。首先,它在每一個節點上除了要保存數據以外,還要額外保存兩個指針;其次,雙向鏈表的各個節點是單獨的內存塊,地址不連續,節點多了容易產生內存碎片。
  > ziplist 因爲是一整塊連續內存,因此存儲效率很高。可是,它不利於修改操做,每次數據變更都會引起一次內存的 realloc。特別是當 ziplist 長度很長的時候,一次 realloc 可能會致使大批量的數據拷貝,進一步下降性能。
  quicklist 內部默認單個 ziplist 長度爲8KB。異步


三、hash(字典)

  • 數據結構:使用「數組+鏈表」的二維結構,至關於Java的HashMap,是一個無序的字典,內部存儲的是鍵值對。
  • 應用場景:通常就是能夠將結構化的數據,好比一個對象(前提是這個對象沒嵌套其餘的對象)給緩存在 Redis 裏,而後每次讀寫緩存的時候,能夠就操做 Hash 裏的某個字段。

  優勢:能夠對信息進行部分獲取,減小網絡流量的浪費。
  缺點:hash 結構的存儲消耗要高於單個字符串,並且咱們如今不少的對象都是比較複雜的,不適應於嵌套對象的場景。
函數

  • 漸進式 rehash 策略
      字典結構內部包含兩個 hashtable,一般狀況下只有一個 hashtable 是有值的,可是在字典擴容或者縮容時,須要分配新的 hashtable,而後進行漸進式搬遷,這時兩個 hashtable 存儲的分別是舊的 hashtable 和新的 hashtable。
      在 rehash 過程當中,查詢會同時查詢兩個 hash 結構。
      待搬遷結束後,舊的 hashtable 被刪除,新的 hashtable 取而代之。

四、set(集合)

  • 數據結構:內部的鍵值對是無序的、惟一的。內部實現至關於一個特殊的字典(hash),字典中全部的 value 都是 NULL。相似於 Java 中的 HashSet。
  • 應用場景
    1. 數據去重
    2. 將兩個set作交集、並集、差集等操做:
      • 交集:sinterstore set12 set1 set2
      • 並集:sunion set1 set2
      • 差集:sdiff set1 set2

五、zset(有序集合)

  • 數據結構:一方面它是一個 set,保證了內部 value 的惟一性,另外一方面它能夠給每個 value 賦予一個 score,表明這個 value 的排序權重,內部使用「跳躍列表」這種數據結構。即內部實現爲一個 hash 字典加一個跳躍列表(skiplist)
  • 應用場景
    1. 排行榜;
    2. 用 zset 來作帶權重的隊列,好比普通消息的 score 爲1,重要消息的 score 爲2,而後工做線程能夠選擇按 score 的倒序來獲取工做任務。讓重要的任務優先執行。

探究「跳躍列表」的底層

普通鏈表查詢「50」這個節點,須要由根節點遍歷鏈表,時間複雜度爲 O(n):

這時,若是咱們給鏈表加一層索引,能夠看到咱們遍歷的節點數變少了:

redis的跳躍列表最多有64層,容納2^64個元素應該不成問題。

跳躍列表——查找過程:從最高層的索引開始遍歷,找到第一個節點(最後一個比「我」小的元素),而後從這個節點開始,降一層再遍歷,找到第二個節點(最後一個比「我」小的元素),以此類推,降到最底層進行遍歷,就能夠找到咱們指望的節點。
  咱們將中間通過的一系列節點稱之爲「搜索路徑」。   

跳躍列表——插入節點

  1. 定位到最底層的數據鏈表要插入的位置
  2. 插入數據
  3. 調整索引

  隨機判斷:對每個新插入的節點,都須要調用一個隨機算法給它分配一個合理的層數,直觀上指望的目標是50%的機率被分配到第一層,25%的機率分配到第二層,以此類推,2^(-63)的機率被分配到最頂層。
  注意的是,若是要插入第n層的索引的時候,同時也要插入1-(n-1)層索引。

  

跳躍列表——刪除節點:和插入過程相似,首先把搜索路徑查出來,定位到數據的位置,刪除數據節點,若是有索引的話,要同時刪除每一層的索引節點。


跳躍列表——更新過程:當咱們調用 zadd 方法時,若是對應的 value 不存在,那就是插入過程。若是這個 value 已經存在了,只是調整了 score 的值,就須要走一個更新流程。假設這個 score 不會帶來排序上的變化,那麼就不須要調整位置,直接修改元素的 score 值就能夠了。不然就對位置進行調整。(簡單的策略就是,先刪除這個元素,再插入這個元素)

這裏就會有一個常見的面試題:爲何 zset 使用跳錶而不使用紅黑樹呢?

  1. 跳錶在代碼上實現起來比紅黑樹簡單;
  2. 跳錶的區間查詢效率會比較高,只須要定位左邊界,而後遍歷就好了。而紅黑樹區間查詢須要經過中序遍歷,效率相對慢;
  3. 紅黑樹要經過左右旋的方式左右子樹的平衡,而跳錶只須要經過隨機函數就能維護這種平衡性。

  跳錶使用空間換時間的設計思路,經過構建多級索引來提升查詢的效率,實現了基於鏈表的「二分查找」。跳錶是一種動態數據 結構,支持快速的插入、刪除、查找操做,時間複雜度都是O(logn)。


其餘高級用法

  1. Bitmap(位圖):支持按 bit 位來存儲信息,能夠用來實現布隆過濾器(BloomFilter);
  2. HyperLogLog:提供不精確的去重計數功能,比較適合用來作大規模數據的去重統計,例如統計 UV(單位時間內有多少個訪問者來到相應的頁面);
  3. Geospatial:能夠用來保存地理位置,並做位置距離計算或者根據半徑計算位置等。

文章最後

  若是你看到最後,相信你也是收穫滿滿。可能有些小夥伴會有疑惑,我不瞭解底層結構,依然能夠很容易上手 redis,那我瞭解其底層的意義是什麼呢?
  我看過這麼一篇推文,推文標題爲: 《 redis探祕:選擇合適的數據結構,減小80%的內存佔用,這些點你get到了嗎?》
  有興趣的小夥伴能夠去看看這篇文章:mp.weixin.qq.com/s/uzuz7rqct…
  最後就是:數據結構沒有最好的,只有最合適的,這就是爲何要了解其數據結構的緣由!

相關文章
相關標籤/搜索