Redis數據結構與對象(上)

數據結構與對象

Redis數據庫中的鍵值對都是由對象構成,而對象是由數據結構構成;其中鍵值對的鍵可以是個字符串對象;值對象可以五選一(字符串、列表、哈希、集合、有序集合);

這塊所說的字符串與平常使用的java中的字符串有些區別,當然主場還是redis本着尊重開發者的原則咱還是說說C字符串比較好;redis中的值對象雖然提供了比較豐富的數據結構,但是學起來也是有一定的難度,儘量先對某一語言的數據結構與算法有一定了解學起來就會比較快,博主之前有些基礎,大概一個多星期就學完了;還是推薦先看看java的數據結構,壯哉我大Java紅黑樹無敵;雖然有序集合這塊算是redis裏面比較不好理解的但是跟那棵樹比基本上就一幼兒園小朋友;

SDS

redis雖然是由C語言實現的但是他只是將C字符串作爲字面量使用,這裏我們用SDS(Simple Dynamic String 簡單動態字符串)來表示字符串。

由sds.h/sdshdr結構表示,共三個屬性

free:用於記錄分配到的額外的未使用空間(下文中的惰性空間釋放策略與空間預分配策略中會用到)

len:記錄字符串長度(有的時候知道自己有多長也是有優勢的)

buf:一個字節數組,前面存字符串,最後一個字節存/0,代表空白字符(個人覺得是向C語言致敬,後文會提到用處)

SDS有哪些優勢

剛纔我們介紹了SDS,現在說說有什麼優勢,其實無論是redis和MC還是rdb和aof,都是要通過比對來判強弱;個人以爲還是Java牛逼(我StringBuilder/StringBuffer不服);--(JVM說了 你SDS再牛我字符串常量池也是你爸爸);

這塊我們以C字符串來和SDS作對比:

  1. 常數時間複雜度獲取字符串長度
    1. C字符串中並未記錄字符串長度,獲取長度需遍歷至字符串結尾,也就是/0,時間複雜度O(n)
    2. SDS提供len屬性,O(1)時間複雜度獲取(沒辦法就是這麼快)
  2. 杜絕緩衝區溢出
    1. C字符串中並未記錄自身長度,增加內容時需先手動擴容後再操作,如果忘記擴容可能會導致溢出到別的緊挨着的字符串中;這樣一下子倆字符串都完了
    2. SDS API在擴展之前會先檢查自身長度是否滿足修改條件,不滿足時先擴容再操作
  3. 減少修改字符串長度時內存重分配次數
    1. C字符串並未記錄自身長度,所以每次增縮操作時,需要對底層的字節數組進行內存重分配
    2. SDS API提供了兩種優化策略
      1. 空間預分配:SDS API會在增長操作時,先檢查是否滿足條件,不滿足便會分配給他足夠的內存空間,並分配一塊額外的內存空間給他續着
      2. 惰性空間釋放:SDS API縮減SDS時,不會一上來就釋放內存空間 ,而是由free屬性記錄要縮減的字節個數,先不刪留着以後再用;就是說內存空間不釋放只是把一部分字節換成了空白的
  4. 二進制安全
    1. C字符串以/0空白字符結尾就註定與二進制安全無緣,程序在讀取C字符串時會認爲讀到空白字符就讀完了;但是二進制數據中有個/0那不是很正常嗎
    2. SDS API天然支持二進制安全,程序不會對讀入的SDS進行任何限制和過濾,讀入時與寫入時一致(吃啥拉啥);而且SDS中提供了兩個屬性
      1. len:可以用來判斷是否空串(肯定比/0好使)
      2. buf:被稱爲二進制字節數組,可以保證安全
  5. 兼容部分C字符串
    1. 因爲SDS結尾也是/0,所以可以使用<String.h>函數庫中的一部分函數,向C語言致敬也算是沒白敬,挺好的

鏈表

redis的鏈表應用還是比較廣泛的(列表對象(高級實現)、發佈與訂閱、慢查詢、監視器等)

redis中的鏈表節點(listNode結構)由一個前驅指針、後繼指針以及自身節點值組成,算是個雙向鏈表

list結構:包含一個表頭節點指針和一個表尾節點指針(header/tail);可以以O(1)的時間複雜度定位到表頭表尾;其中表頭節點的前驅指針和表尾節點的後繼指針指向Null,也就是說redis中的鏈表是一個無環的雙向鏈表

redis爲不同類型的數據提供了不同類型的特定函數,所以鏈表可以支持多種豐富的數據類型

字典

Redis的底層由字典實現,對於redis的CRUD就是基於對字典的操作

字典的底層是由哈希表構成 ,哈希表又由多個哈希表節點構成每個哈希表節點都存了一個字典的鍵值對

哈希表

哈希表由dict.h/dictht結構表示 有4個屬性

table:是一個數組,數組中的每個數組項都是一個指向一個dictEntry結構的指針,每個dictEntry結構都保存着一個鍵值對

size:記錄哈希表的長度即數組長度

used:記錄哈希表中哈希表節點的個數(有多少個鍵值對)

sizemask:總是size-1,與索引值共同確定一個鍵應該被放到table數組的哪個索引上

哈希表節點

哈希表節點由dict.h/dictEntry結構表示 有3個屬性

key:鍵值對的鍵

v:鍵值對的值(值可以是個指針,也可以是uint64_t/int64_t整數)

next:指向下一個節點的指針

字典

字典由dict.h/dict結構來表示 有幾個屬性

type和privdata屬性是爲了不同類型的鍵值對,爲實現多態字典而設置的屬性

type:是一個指向一個dictType結構的指針,每個dictType結構都包含一組用於處理多種不同類型鍵值對的函數;Redis爲多態字典設置提供了不同類型的特定函數

privdata:用於存放傳給那些不同類型特定函數的可選參數

ht[]:字典中最重要的屬性還是得提到ht,ht是一個數組項但是隻有兩個項(兩個哈希表),ht[0],ht[1];正常情況下字典只會對ht[0]進行CRUD操作,ht[1]只會在對ht[0]rehash時使用;

rehashidx:該屬性也是與rehash有關,可以說是rehash的進度條;俗稱索引計數器變量;非rehash時值爲-1(下文還有)

rehash(重新散列)

負載因子:used/size  哈希表中元素個數/哈希表的長度

作用:對哈希表進行擴容縮容

爲了維持負載因子在一個比較正常的範圍對哈希表進行擴容縮容操作,也就是說在哈希表中的哈希表節點個數過多過少時進行rehash,當然不止這一個因素,下文會具體講解

rehash一步肯定完不了大概需要三步:

  1. 爲ht[1]分配內存空間,分配多大取決於具體操作(擴還是縮)以及ht[0]的鍵值對個數(used屬性)
    1. 擴容:ht[1]size大於等於第一個ht[0]used*2的2^n
    2. 縮容:ht[1]size大於等於第一個ht[0]used的2^n
  2. 將ht[0]中的所有鍵值對rehash到ht[1]上,rehash:重新計算哈希值與索引值並將鍵值對放到table數組指定的位置上
  3. 將ht[0]中的所有鍵值對rehash到ht[1]之後,將ht[1]設置爲ht[0],並在ht[1]位置上重新創建一個哈希表爲下一次rehash做準備

漸進式rehash

分多次的漸進的將ht[0]中的所有鍵值對rehash到ht[1]中

爲什麼要有漸進式rehash呢,因爲在哈希表中鍵值對數量極大時比如幾億個,一次性rehash到ht[1],會造成龐大的計算量,可能會導致服務一段時間內停止

漸進式rehash運行時四個步驟:

  1. 爲ht[1]分配內存空間,字典同時持有ht[1]ht[0]兩張哈希表
  2. 設置字典的rehashidx索引計數器變量值,將該值設置爲0,證明rehash正式開始
  3. 每次對字典進行CRUD操作時,除了處理正常操作之外,還需要將rehashidx索引上的所有鍵值對rehash到ht[1]中,本次階段性rehash成功後,將rehashidx的值+1
  4. 隨着操作不斷的進行,最終必定會在某一時間點將ht[0]中的所有鍵值對rehash到ht[1],此時再將rehashidx的值設置爲-1,證明rehash結束

優勢:與一次性rehash相比,將rehash分配在各個CRUD操作中,可以分多次rehash,避免對服務器造成大量計算負擔

漸進式rehash時對哈希表的操作:

新增操作和別的不太一樣:只會在ht[1]中新增元素,保證ht[0]中的used值有減不增

查詢等操作都是先去ht[0]中查找,要是沒有就去ht[1]中操作

何時開始rehash

滿足以下條件時,對哈希表進行擴容操作

  1. 當服務器未執行BGSAVE和BGREWITEAOF命令時,並且負載因子大於等於1;
  2. 當服務器執行BGSAVE和BGREWITEAOF命令時,並且負載因子大於等於5;

爲什麼這兩種情況下的負載因子的要求不一樣呢,因爲服務器進程在執行BGSAVE和BGREWITEAOF命令時,會創建一條子進程來執行創建RDB文件/AOF後臺重寫,會執行大量對磁盤的寫入操作,將負載因子上限調大也是爲了避免此時發生擴容給服務器帶來更大的壓力,所以服務器希望在未執行該命令時完成擴容

當負載因子小於0.1時,對哈希表進行縮容操作

跳錶

跳錶在redis中引用的並不算十分廣泛,只用在有序集合的實現中作爲組件出現;再有就是集羣中子節點的數據結構;

跳錶又稱跳躍表;它是從單鏈表中抽出索引層,索引層越多效率越高,也就是具備了多級索引層的鏈表,與實現二分查找算法的數組較爲相似;

跳錶支持最好情況下O(logN) 最壞情況下O(n) 的查找時間複雜度

跳錶節點由redis.h/zskiplistNode結構定義 有以下屬性

level(層):每個層都是一個數組,包含多個元素比如(前進指針和跨度值),層越多,跳錶查詢的速度越快

forword:前進指針,從表頭方向指向表尾方向的一個指針,可以用於表頭向表尾方向的訪問

span:跨度值,用於記錄前進指針所指向的節點與當前節點的距離

  1. 跨度值越大,距離越遠
  2. 前進指針爲NUll時,跨度值爲0 ,因爲沒有指向任何節點

backword :後退指針,從表尾方向指向表頭的指針,與前進指針不同的是,後退指針每次只能退一個節點,因爲每層只有一個後退指針,卻可能包含多個前進指針

score:分值;一個double類型的浮點數值。用於記錄元素的分值,跳錶中按照分值大小從小到大排序

obj:成員對象;一個指向了sds字符串的指針

在跳錶中,每個節點的obj必須是唯一的,但是節點的分值可以相同,分值相同時,按照成員對象在字典序中的大小排列,小的在前(接近表頭方向),大的靠後(靠近表尾方向)

跳錶由redis.h/zskiplist結構定義

其實由多個跳錶節點也可以組成跳錶,但是比不上zskiplist結構的跳錶;

zskiplist中提供了幾個屬性:

header/tail:表頭節點指針與表尾節點指針,支持O(1)的時間複雜度查找定位表頭表尾節點

length:跳錶中節點個數

level:跳錶中具備最大層級的節點(O(1)時間複雜度定位)

整數集合

整數集合可以作爲集合對象的底層實現;有所限制集合中元素只能是整數值,並且不能有任何重複元素存在

由intset.h/intset結構定義:

contents:是一個保存元素的數組,集合中的每個元素都是contents數組的一個數組項(item),各個項在數組中按值從小到大有序的排列,並且數組中不包含任何重複項

length:記錄了集合的元素數量(contents數組的長度)

encoding:

  • 編碼方式,用於決定contents數組的類型
    • 如果encoding屬性的值爲INTSET_ENC_INT16,那麼contents就是一個int16_t類型的數組(2個字節16位)
    • 如果encoding屬性的值爲INTSET_ENC_INT32,那麼contents就是一個int32_t類型的數組(4個字節32位)
    • 如果encoding屬性的值爲INTSET_ENC_INT64,那麼contents就是一個int64_t類型的數組(8個字節64位)

升級:

    1. 何時升級:當新元素加入整數集合並且該元素比現有所有元素的類型都要長時,進行升級,之後添加新元素
    2. 三步驟:
      • 根據新元素的類型,擴展contents數組大小,併爲其分配空間
      • 將contents數組中現有元素都轉換爲新元素的類型,並按照值的大小從小到大排列於數組中
      • 將新元素添加到contents數組
    3. 升級的好處:
      • 提升靈活性
        1. 整數集合可以自動升級底層數組來適應新元素,所以不用擔心出現類型錯誤
      • 節約內存
        1. 底層數組一直都是保持一種類型,只有新元素的類型比現有元素類型長時纔會升級
    4. 整數集合不支持降級操作

壓縮列表

壓縮列表可以作爲列表對象、哈希對象、有序集合對象的底層實現,他是redis爲了節省內存而實現的,由一系列特殊編碼組成的內存塊構成的順序型數據結構(可以當做是個數組)

壓縮列表 ziplist

zlbytes:記錄壓縮列表佔用的內存字節數

zllen:記錄壓縮列表包含的節點數量

entryX:記錄壓縮列表中所有的節點

zltail:記錄表尾節點到起始地址有多少字節

zlend:用於標記壓縮列表的末端

壓縮列表節點 ziplistNode

previous_entry_length屬性:以字節爲單位,記錄了壓縮列表中前一個節點的長度

  1. 如果前一節點的長度<254字節,長度爲1字節
  2. 如果前一節點的長度>=254字節,長度爲5字節

encoding屬性:記錄了節點的類型以及長度

content屬性:負責保存節點的值,節點的值可以是個字節數組(字符串)或者整數

連鎖更新:添加新節點到壓縮列表,或者從壓縮列表中刪除節點,可能會引發連鎖更新操作,但這種操作出現的機率並不高

下一篇再見!感謝閱讀者閱讀並提出建議!罵都可以就是別帶髒字!下一篇咱們接着說對象,今兒就先歇了

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章