redis的五種數據結構原理分析


redis的五種數據結構原理分析

本章主要內容

  • 簡單介紹redis
  • redis中的五種數據結構分析
  • 應用場景分析
  • 總結

 

關於Redis

redis是一個開源的使用C語言編寫的一個kv存儲系統,是一個速度非常快的非關系遠程內存數據庫。它支持包括String、List、Set、Zset、hash五種數據結構。除此之外,通過復制、持久化和客戶端分片等特性,用戶可以很方便地將redis擴展成一個能夠包含數百GB數據和每秒處理上百萬次的請求的系統。目前支持多種語言的api,方便用戶使用。

redis同時也內置了事務、LUA腳本、復制等功能,提供兩種持久化選項,一種是每隔一段時間將數據導入到磁盤(快照模式),另一種是追加命令到日志中(AOF模式)。如果只是作為高效的內存數據庫使用也可以關閉持久化功能。通過哨兵(sentinel)和自動分區(Cuuster)的方式可以提高redis服務器的高可用性。

與關系型數據庫相比,redis的命令請求不需要經過查詢分析器或查詢優化器進行處理,也避免了更新數據時引起的隨機讀\寫,這些慢操作。它直接讀寫內存中的數據,並且數據是按照一定的數據結構存儲的。所以它的速度非常快。

 

五種數據結構

字符串(String)

       與其它編程語言或其它鍵值存儲提供的字符串非常相似,鍵(key)------值(value) (字符串格式),字符串擁有一些操作命令,如:get set del 還有一些比如自增或自減操作等等。redis是使用C語言開發,但C中並沒有字符串類型,只能使用指針或符數組的形式表示一個字符串,所以redis設計了一種簡單動態字符串(SDS[Simple Dynamic String])作為底實現:

定義SDS對象,此對象中包含三個屬性:

  • len buf中已經占有的長度(表示此字符串的實際長度)
  • free buf中未使用的緩沖區長度
  • buf[] 實際保存字符串數據的地方

所以取字符串的長度的時間復雜度為O(1),另,buf[]中依然采用了C語言的以\0結尾可以直接使用C語言的部分標准C字符串庫函數。

空間分配原則:當len小於IMB(1024*1024)時增加字符串分配空間大小為原來的2倍,當len大於等於1M時每次分配 額外多分配1M的空間。

由此可以得出以下特性:

  • redis為字符分配空間的次數是小於等於字符串的長度N,而原C語言中的分配原則必為N。降低了分配次數提高了追加速度,代價就是多占用一些內存空間,且這些空間不會自動釋放。
  • 二進制安全的
  • 高效的計算字符串長度(時間復雜度為O(1))
  • 高效的追加字符串操作。

 

列表(List)

         redis對鍵表的結構支持使得它在鍵值存儲的世界中獨樹一幟,一個列表結構可以有序地存儲多個字符串,擁有例如:lpush lpop rpush rpop等等操作命令。在3.2版本之前,列表是使用ziplist和linkedlist實現的,在這些老版本中,當列表對象同時滿足以下兩個條件時,列表對象使用ziplist編碼:

  • 列表對象保存的所有字符串元素的長度都小於64字節
  • 列表對象保存的元素數量小於512個

當有任一條件 不滿足時將會進行一次轉碼,使用linkedlist。

而在3.2版本之后,重新引入了一個quicklist的數據結構,列表的底層都是由quicklist實現的,它結合了ziplist和linkedlist的優點。按照原文的解釋這種數據結構是【A doubly linked list of ziplists】意思就是一個由ziplist組成的雙向鏈表。那么這兩種數據結構怎么樣結合的呢?

ziplist的結構

         由表頭和N個entry節點和壓縮列表尾部標識符zlend組成的一個連續的內存塊。然后通過一系列的編碼規則,提高內存的利用率,主要用於存儲整數和比較短的字符串。可以看出在插入和刪除元素的時候,都需要對內存進行一次擴展或縮減,還要進行部分數據的移動操作,這樣會造成更新效率低下的情況。

這篇文章對ziplist的結構講的還是比較詳細的:

https://blog.csdn.net/yellowriver007/article/details/79021049

linkedlist的結構

        意思為一個雙向鏈表,和普通的鏈表定義相同,每個entry包含向前向后的指針,當插入或刪除元素的時候,只需要對此元素前后指針操作即可。所以插入和刪除效率很高。但查詢的效率卻是O(n)[n為元素的個數]。

了解了上面的這兩種數據結構,我們再來看看上面說的“ziplist組成的雙向鏈表”是什么意思?實際上,它整體宏觀上就是一個鏈表結構,只不過每個節點都是以壓縮列表ziplist的結構保存着數據,而每個ziplist又可以包含多個entry。也可以說一個quicklist節點保存的是一片數據,而不是一個數據。總結:

  • 整體上quicklist就是一個雙向鏈表結構,和普通的鏈表操作一樣,插入刪除效率很高,但查詢的效率卻是O(n)。不過,這樣的鏈表訪問兩端的元素的時間復雜度卻是O(1)。所以,對list的操作多數都是poll和push。
  • 每個quicklist節點就是一個ziplist,具備壓縮列表的特性。

在redis.conf配置文件中,有兩個參數可以優化列表:

  1. list-max-ziplist-size 表示每個quicklistNode的字節大小。默認為-2 表示8KB
  2. list-compress-depth 表示quicklistNode節點是否要壓縮。默認是0 表示不壓縮

哈希(hash)

        redis的散列可以存儲多個鍵 值 對之間的映射,散列存儲的值既可以是字符串又可以是數字值,並且用戶同樣可以對散列存儲的數字值執行自增操作或者自減操作。散列可以看作是一個文檔或關系數據庫里的一行。hash底層的數據結構實現有兩種:

  • 一種是ziplist,上面已經提到過。當存儲的數據超過配置的閥值時就是轉用hashtable的結構。這種轉換比較消耗性能,所以應該盡量避免這種轉換操作。同時滿足以下兩個條件時才會使用這種結構:
    • 當鍵的個數小於hash-max-ziplist-entries(默認512)
    • 當所有值都小於hash-max-ziplist-value(默認64)
  • 另一種就是hashtable。這種結構的時間復雜度為O(1),但是會消耗比較多的內存空間。

 

集合(Set)

         redis的集合和列表都可以存儲多個字符串,它們之間的不同在於,列表可以存儲多個相同的字符串,而集合則通過使用散列表(hashtable)來保證自已存儲的每個字符串都是各不相同的(這些散列表只有鍵,但沒有與鍵相關聯的值),redis中的集合是無序的。還可能存在另一種集合,那就是intset,它是用於存儲整數的有序集合,里面存放同一類型的整數。共有三種整數:int16_t、int32_t、int64_t。查找的時間復雜度為O(logN),但是插入的時候,有可能會涉及到升級(比如:原來是int16_t的集合,當插入int32_t的整數的時候就會為每個元素升級為int32_t)這時候會對內存重新分配,所以此時的時間復雜度就是O(N)級別的了。注意:intset只支持升級不支持降級操作。

intset在redis.conf中也有一個配置參數set-max-intset-entries默認值為512。表示如果entry的個數小於此值,則可以編碼成REDIS_ENCODING_INTSET類型存儲,節約內存。否則采用dict的形式存儲。

 

有序集合(zset)

        有序集合和散列一樣,都用於存儲鍵值對:有序集合的鍵被稱為成員(member),每個成員都是各不相同的。有序集合的值則被稱為分值(score),分值必須為浮點數。有序集合是redis里面唯一一個既可以根據成員訪問元素(這一點和散列一樣),又可以根據分值以及分值的排列順序訪問元素的結構。它的存儲方式也有兩種:

  • 是ziplist結構。

          與上面的hash中的ziplist類似,member和score順序存放並按score的順序排列

  • 另一種是skiplist與dict的結合。

         skiplist是一種跳躍表結構,用於有序集合中快速查找,大多數情況下它的效率與平衡樹差不多,但比平衡樹實現簡單。redis的作者對普通的跳躍表進行了修改,包括添加span\tail\backward指針、score的值可重復這些設計,從而實現排序功能和反向遍歷的功能。

一般跳躍表的實現,主要包含以下幾個部分:

  • 表頭(head):指向頭節點
  • 表尾(tail):指向尾節點
  • 節點(node):實際保存的元素節點,每個節點可以有多層,層數是在創建此節點的時候隨機生成的一個數值,而且每一層都是一個指向后面某個節點的指針。
  • 層(level):目前表內節點的最大層數
  • 長度(length):節點的數量。

跳躍表的遍歷總是從高層開始,然后隨着元素值范圍的縮小,慢慢降低到低層。

跳躍表的實現原理可以參考:https://blog.csdn.net/Acceptedxukai/article/details/17333673

前面也說了,有序列表是使用skiplist和dict結合實現的,skiplist用來保障有序性和訪問查找性能,dict就用來存儲元素信息,並且dict的訪問時間復雜度為O(1)。

 

應用場景

redis一般應用場景

  1. 緩存會話(單點登錄)
  2. 分布式鎖,比如:使用setnx
  3. 各種排行榜或計數器
  4. 商品列表或用戶基礎數據列表等
  5. 使用list作為消息對列
  6. 秒殺,庫存扣減等

 

五種類型的應用場景

  1. String,redis對於KV的操作效率很高,可以直接用作計數器。例如,統計在線人數等等,另外string類型是二進制存儲安全的,所以也可以使用它來存儲圖片,甚至是視頻等。
  2. hash,存放鍵值對,一般可以用來存某個對象的基本屬性信息,例如,用戶信息,商品信息等,另外,由於hash的大小在小於配置的大小的時候使用的是ziplist結構,比較節約內存,所以針對大量的數據存儲可以考慮使用hash來分段存儲來達到壓縮數據量,節約內存的目的,例如,對於大批量的商品對應的圖片地址名稱。比如:商品編碼固定是10位,可以選取前7位做為hash的key,后三位作為field,圖片地址作為value。這樣每個hash表都不超過999個,只要把redis.conf中的hash-max-ziplist-entries改為1024,即可。
  3. list,列表類型,可以用於實現消息隊列,也可以使用它提供的range命令,做分頁查詢功能。
  4. set,集合,整數的有序列表可以直接使用set。可以用作某些去重功能,例如用戶名不能重復等,另外,還可以對集合進行交集,並集操作,來查找某些元素的共同點
  5. zset,有序集合,可以使用范圍查找,排行榜功能或者topN功能。

 

總結

       本章介紹了redis的五種數據結構和它們使用的底層存儲原理,為了達到節省內存和快速訪問的目的每種數據結構可能有兩種存儲和訪問結構,在必要的時候會由一種結構轉換成另一種結構,但這個轉換的過程會消耗系統性能和內存空間的,所以在使用的過程中需要注意這些配置參數,開發中盡量避免達到這些峰值,使得redis能夠持續的提供高效的服務。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM