zset如何解決內部連結串列查詢效率低下

煙花散盡13141發表於2021-07-12

zset作為有序集合,內部基於跳錶或者說索引的方式實現了資料的快速查詢。解決了連結串列查詢效率低下的痛點

前言

  • 緊接前文我們學習了Redis中Hash結構。在裡面我們梳理了字典這個重要的內部結構並分析了hash結構rehash的流程從而解釋了為什麼redis單執行緒還是那麼快
  • 本章節我們將視角下推,繼續學習Redis五大天王中的zset資料結構 ; zset是有序不重複集合其內部元素唯一且是有序的,他的排序標準是根據其內部score維度進行排序的。

zset結構

image-20210705144222654

基本單元

  • 關於zset結構很簡單,一個是我們之前學習的字典結構(簡單理解成Hash結構),另外一個是跳躍表結構 ; 關於字典我們上一章節已經詳細解說了其內部的構造及其如何進行資料擴容等操作!剩下的且符合今天我們學習主旨的自然就是這個熟悉又陌生的zskiplist
  • 我們根據上面zset的結構圖也能夠看出來,zskiplist實際上就是一個連結串列。

zskiplist

image-20210705145402021

  • 我們檢視原始碼不難看出其內部結構是對zset中連結串列的一個抽象描述。zskiplist首先會對這個連結串列記錄其頭結點、尾結點方便通過zskiplist進行遍歷操作。剩下的length自然就是對內部的這個連結串列數量的統計。比較抽象的是這個level的理解。在上面我們也看到了zskiplist那個連結串列實際上會有分層的概念。筆者這裡通過不同的顏色進行表述不同層級的概念。
  • 筆者這裡針對上述描述的跳躍表內部的zskiplist繪畫了一張內部資料圖

image-20210705145952498

  • 在對zskiplist結構描述和資料描述中我將他們拆開理解,覺得這樣更容易理解結構關係。下面是整個圖示

image-20210705150306846

  • 細心的讀者應該能夠發現,我好想漏掉了連結串列重要組成部分zskiplistNode這個重要的節點說明。實際上他就是我們右側那個連結串列中節點。換句話說連結串列中每個點就是zskiplistNode 。

level

  • 跳躍表的重要特性型別與樹結構可以避免逐個遍歷的苦惱。那麼他是如何實現這種跳躍性質的訪問的呢?還有一點為什麼redis會這麼設計。首先我們先回答下為什麼這麼設計。在連結串列中插入、刪除等操作是很快速的只需要改變指標指向就可以完成。但是對於查詢來說他需要遍歷整個連結串列才能完成操作。針對連結串列的這個弊端redis設計了跳錶的資料結構。
  • 下面就是針對如何實現來簡單梳理下。上述zskiplistNode節點物件結構中我們也可以看到有個level屬性。redis就是通過這個屬性來實現跳躍的特性。在每個節點生成的時候回隨機生成這個level值。他就表示這個節點所在層的範圍。
  • 關於這個level為什麼說是隨機。這有牽涉到其內部的冪等性演算法。這個演算法保證數字越大生成的概覽越小。在redis內部level最大值32.
  • 比如說level隨機生成5 。 表示當前節點node在level1~level5這五層中。上面的圖示中所示的三個節點生成的level值分別是level3、level2、level7。注意在實際儲存中level索引時從0開始。

forward

  • 在level中還有兩個屬性分別是前進指標、跨越長度。根據字面意思我們能夠理解前進指標是想連結串列後端方向推進的指標。其跨度就是表示當前節點距離前進指標處節點的距離。這個距離的是參考最底層的距離的。

image-20210705152058003

雙連結串列

  • 在zskiplist中每一層都是一個單向連結串列。在level中通過forwar指標指向我們表尾。那麼為什麼我說是雙連結串列呢?這裡的雙連結串列不是嚴格意義的雙連結串列。但是我們可以藉助這些層級的單連結串列實現我們雙向自由路由。

image-20210705153433365

隨機層

image-20210705152812968

  • 上面我們已經解釋過level的定義了。那麼為什麼這裡還有再提一遍呢?因為上面我們簡單提到了冪等性演算法。這裡我們就詳細解釋下什麼是冪等性。

  • 首先根據level的定義我們可以總結如下幾點關於level的特性。

  • ①、一個節點如果在level[i]中,那麼他一定在level[i]以下的層中

  • ②、越高層元素跨度越大,這個跨度是不定的。取決於生成節點時的隨機演算法

  • ③、每一層都是一個連結串列

image-20210705153545606

  • 這是redis中原始碼部分。關於這個隨機level的演算法其實不是很難理解。筆者這裡將上述程式碼進行流程化梳理

image-20210705162650582

  • 就是一個不斷重試的機制。其中p和maxLevel都是程式碼中的固定值。在這個演算法機制下我們就可以儘可能的保證在資料量小的情況下保證level不會特別的高。
  • 換句話說我們的level就不會顯得特別的突兀。如果是純粹的隨機生成的話就有可能有的節點level很低,有的level很高。這樣會造成資源不必要的浪費。

查詢

  • 好了,同學們到了這裡我們已經學習了關於zset的基本結構。 簡單回顧下內部就是字典+跳錶的結合。下面我們針對這兩種資料結構來簡單梳理下關於zset的常用的一些操作!
  • 首先就是我們的查詢。上面說了那麼多內部結構。紙上談兵終覺淺,我們還需要實戰操作一下。

分數定位

image-20210705164350328

  • 上述的命令基本都是通過分數定位然後在做自己的業務處理。

image-20210705165800181

  • 圖示中已經說明了我們過程。首先是在最高層中尋找因為高層最稀疏。當高層沒有發現時我們就會下推層級。此時我們來到level中的節點1.然後在通過forwar指標進行前移。最終定位節點5。
  • 還有一點補充說明:節點中通過obj指標指向實際內容,score儲存分支;筆者這裡為什麼演示方便直接在節點中標註了分數。其他部分並未進行標註!!!

成員定位

  • 筆者在整理相關邏輯的時候也是經過百度、視訊、書籍等方式翻閱後作出的結論。原諒我的能力無法直接閱讀原始碼!但是在查閱資料的過程中。發現很少有說明是如何進行成員定位的。因為zset中除了分數的相關命令以外還有不少是基於成員定位的。

image-20210705170513847

  • 上述命令部分是基於成員進行定位的。在zset結構中實際節點是有基於score進行排序的。在obj中沒有順序可言。我們無法按照我們上述通過分數進行逐層定位元素!這就牽扯到我們另外一個重要的角色【字典(hash)】了。

image-20210705171027960

  • 上圖是我們上一章節的關於字典的說明。在通過成員定位的時候我們就是多了一步先從字典中定位到分數,然後在重複上面的步驟進行定位!

image-20210705171201050

  • 瞭解結構自然就能很容易理解相關的操作。站在巨人的肩膀我們雖然不需要在重複的造輪子了。但是我們得知道當初前輩們造輪子的過程!吃水不忘挖井人!

命令內部理解

  • 瞭解結構就能快速掌握命令,否則就算死記硬背命令過一陣子又會忘記了。但是牢記結構後我們就會知道有命令可以實現我們的需求然後根據手冊就可以得心應手。下面我們看看一下四個命令是如何實現的吧。

zcard

  • 通過zskiplist中length屬性

zcount

  • 通過分數定位邊界,然後遍歷底層連結串列最終得到統計數量

zlecount

  • 通過字典定位分數,在執行zcount操作

zrank

  • 返回有序集合中指定成員的索引 , 先定位成員,定位過程中通過span可以確定排名

總結

  • zset是一種有序連結串列。為了解決連結串列查詢低下從而redis構建了跳錶的資料結構。大大提高了效率!
  • 關於zset的資料結構我們實際好多案例可以通過它來實現;延時佇列、內部LRU、熱點資料等等

點贊、關注不迷路哦


相關文章