Redis系列(九)底層數據結構之五種基礎數據類型的實現

前言

Redis 已是你們耳熟能詳的東西了,平常工做也都在使用,面試中也是高頻的會涉及到,那麼咱們對它究竟瞭解有多深入呢?html

我讀了幾本 Redis 相關的書籍,嘗試去了解它的具體實現,將一些底層的數據結構及實現原理記錄下來。面試

本文將介紹 Redis 中 五種基礎數據類型 的實現方法。 這五種基本類型基本覆蓋了咱們業務中使用的 80%的場景,對面試也覆蓋至少 90%.(其中重點固然是有序集合以及散列結構咯).redis

定義

在前面的八篇文章中,咱們詳細的介紹了 Redis 中的 8 種基本數據結構,可是衆所周知,Redis 經常使用的數據類型有五種。包括,字符串,列表,集合,有序集合,哈希。數據庫

而這五種數據類型,底層就是用前面介紹的數據結構實現的,固然,並非直接一對一的綁定關係,而是採用了精妙的設計,構建了一個對象系統。編程

熟悉 OOP 編程的讀者,可能很快就能想到爲何要這麼設計了,對象系統帶來的好處是很是多的,可是並不在這一篇文章中講。這裏只是提到對象系統,讓你們對於五種數據類型爲何能夠用一些花裏胡哨的方法來實現,有一個初步的瞭解。後端

接下來將逐一分析五種數據類型的底層實現數據結構,及實現方式(編碼)之間的切換條件。微信

注:後續提到五種數據類型,用 xx 對象來指代。好比 字符串對象,列表對象。提到的底層數據結構,用全稱來說。markdown

字符串對象

涉及到的數據結構,SDS, 強烈建議閱讀本系列第一篇文章。數據結構

字符串對象的底層實現有三種可能:int, raw, embstr.性能

int

若是一個字符串對象,保存的值是一個整數值,而且這個整數值在 long 的範圍內,那麼 redis 用整數值來保存這個信息,而且將字符串編碼設置爲 int.

好比:

2020-01-12-16-13-35

raw

若是字符串對象保存的是一個字符串, 而且長度大於 32 個字節,它就會使用前面講過的SDS(簡單動態字符串)數據結構來保存這個字符串值,而且將字符串對象的編碼設置爲raw.

2020-01-12-16-16-26

embstr

若是字符串對象保存的是一個字符串, 可是長度小於 32 個字節,它就會使用embstr來保存了,embstr編碼不是一個數據結構,而是對 SDS 的一個小優化,當使用 SDS 的時候,程序須要調用兩次內存分配,來給 字符串對象 和 SDS 各自分配一塊空間,而embstr只須要一次內存分配,由於他須要的空間不多,因此採用 連續的空間保存,即將 SDS 的值和 字符串對象的值放在一塊連續的內存空間上。這樣能在短字符串的時候提升一些效率。

好比:

2020-01-12-16-21-20

浮點數如何保存?

redis 的字符串數據類型是支持保存浮點數,而且支持對浮點數進行加減操做,可是 redis 在底層是把浮點數轉換成字符串值,以後走上面三種編碼的規則的。對浮點數進行操做時,也是從字符串轉換成浮點數進行計算,而後再轉換成字符串進行保存的。

編碼轉換條件

這塊的知識實際上是很符合咱們的認知的,好比 int編碼只能夠保存整數,那麼當咱們對一個 int 編碼的字符串對象,修改它的值,它天然就會使用 raw 編碼了。

可是有一個特性,Redis 沒有爲embstr編碼提供任何的修改操做,這也就是爲何它只是個編碼而不是一個數據結構的緣由。

因此在 Redis 中,embstr編碼的值實際上是 只讀的,只要發生修改,馬上將編碼轉換成 raw.

總結

字符串對象底層的數據結構或者說編碼有三種,分別是 int, raw, embstr. 他們之間的使用條件以下:

編碼 使用條件
int 能夠用 long 保存的整數
embstr 字符串長度小於 32 字節(或者浮點數轉換後知足)
raw 長度大於 32 的字符串

列表對象

涉及到的數據結構,壓縮列表, 雙向鏈表, 快速列表, 強烈建議閱讀本系列的第 二,三,四 篇文章。

在 Redis 3.2 版本以前,列表對象底層由 壓縮列表和雙向鏈表配合實現,當元素數量較少的時候,使用壓縮列表,當元素數量增多,就開始使用普通的雙向鏈表保存數據。

可是這種實現方式不夠好,雙向鏈表中的每一個節點,都須要保存先後指針,這個內存的使用量 對於 Redis 這個內存數據庫來講極其不友好。

所以在 3.2 以後的版本,做者新實現了一個數據結構,叫作 quicklist. 全部列表的底層實現都是這個數據結構了。它的底層實現基本上就是將 雙向鏈表和壓縮列表進行告終合,用雙向的指針將壓縮列表進行鏈接,這樣不只避免了壓縮列表存儲大量元素的性能壓力,同時避免了雙向鏈表鏈接指針佔用空間過多的問題。

具體的原理講解請 閱讀對應的文章,這裏再也不贅述。

總結

編碼 使用條件
quicklist 全部數據

集合對象

涉及到的數據結構:intset, dict(hashtable), 強烈建議閱讀本系列第五,第六篇文章。

集合對象的編碼能夠是 intset 或者 hashtable(字典) .

intset

當集合中的全部元素都是整數,且元素的數量不大於 512 個的時候,使用 intset 編碼。

2020-01-12-16-46-34

intset 編碼時,底層使用 intset數據結構。

hashtable

當元素不符合所有爲整數值且元素個數小於 512時,集合對象使用的編碼方式爲** hashtable**.

字典的每個鍵都是一個字符串對象,其中保存了集合裏的一個元素,字典的值所有被設置爲 NULL.

2020-01-12-16-54-33

總結

編碼 使用條件
intset 全部元素都是整數且元素個數小於 512
hashtable 其餘數據

有序集合對象

涉及到的數據結構,壓縮列表, 跳躍表, 字典, 強烈建議閱讀本系列 第三篇,第六篇,第七篇文章。

有序集合對象的編碼能夠是 ziplist 以及skiplist.

ziplist 編碼

當使用 ziplist 編碼時,有序集合對象的實現數據結構爲ziplist(聽起來像句廢話), 每一個集合的元素 (key-value), 使用兩個緊挨着的壓縮列表的節點來表示,第一個節點保存集合元素的成員 (member), 第二個節點保存集合元素的分支 (score).

在壓縮列表的內部,集合元素按照分值從小到大進行排序。

2020-01-12-17-05-27

skiplist 編碼

當使用 skiplist 編碼的時候,內部使用zset 來實現數據的保存,zset的定義以下:

typedef struct zset{
  zskiplist *zsl;
  dict *dict;
}zset;
複製代碼

爲何須要同時使用跳躍表以及字典呢?

其實若是咱們細想,單獨使用字典或者跳躍表,都是能夠實現有序集合的全部功能的,可是性能太差勁了。

  • 當咱們只使用字典來實現,咱們能夠以 O(1) 的時間複雜度獲取成員的分值,可是因爲字典是無序的,當咱們須要進行範圍性操做的時候,須要對字典中的全部元素進行排序,這個時間複雜度至少須要 O(nlogn).
  • 當咱們只使用跳躍表來實現,咱們能夠在 O(logn) 的時間進行範圍排序操做,可是若是要獲取到某個元素的分值,時間複雜度也是 O(logn).

所以,將字典和跳躍表結合進行使用,能夠在 O(1) 的時間複雜度下完成查詢分值操做,而對一些範圍操做,使用跳躍表能夠達到 O(logn) 的是纏綿複雜度。

2020-01-12-17-14-17

能夠看到,我在上一次的例子中,添加了一個很長的 key 以後,有序集合的編碼方式就成爲了skiplist.

總結

編碼 使用條件
ziplist 元素數量少於 128 且 全部元素成員的長度小於 64 字節
skiplist 不知足上述條件的其餘狀況

散列對象

涉及到的數據結構,壓縮列表, 字典, 強烈建議閱讀本系列 第三篇,第六篇文章。

哈希對象的編碼能夠是ziplist或者hashtable.

ziplist 編碼

ziplist 編碼下的哈希對象,使用了壓縮列表做爲底層實現數據結構,用兩個連續的壓縮列表節點來表示哈希對象中的一個鍵值對。實現方式相似於上面的有序集合的場景。

2020-01-12-17-21-29

如圖中所示,當我放入了兩個簡單的鍵值對,此時哈希對象的編碼爲 ziplist.

hashtable 編碼

這是對 hashtable 最直觀的應用了~

哈希結構自己在結構上和字典 (hashtable) 就頗爲類似,所以哈希對象中的每個鍵值對都是字典中的一個鍵值對。

  • 字典的每個鍵都是一個字符串對象,對象中保存了鍵值對的鍵。
  • 字典的每個值都是一個字符串對象,對象中保存了鍵值對的值。

2020-01-12-17-25-32

如圖中所示,當我在上一個示例中額外加入一個很長的值,那麼編碼方式就來到了hashtable.

總結

編碼 使用條件
ziplist 鍵值對的鍵和值的長度都小於 64 字節,且 鍵值對個數小於 512.
hastable 不知足上述條件的其餘條件

全文總結

其實在前面的幾篇文章寫完以後,也就是在全部的底層數據結構介紹完以後,所謂的Redis 的五種基礎數據類型的底層實現原理就已經沒有了難度。

全部用到的底層數據結構都知道了,剩下的無非是個排列組合問題以及各類實現方式之間的切換條件,而後這個條件也僅僅是瞭解性知識,強行記住也沒有必要。

這裏把五種基礎數據類型的可能的編碼列出來方便理解及記憶。

基礎數據類型 可能的編碼方式
字符串 int, raw, embstr
列表 以前是 ziplist 和 linkedlist, 如今全是 quicklist 了。
集合 intset 或者 hashtable
有序集合 ziplist 或者 skiplist, skiplist 編碼中使用了跳躍表+字典
散列 ziplist 或者 hashtable

至於他們的轉換條件,因爲我不會用 markdown 畫多維表格,可是又不想寫 html, 就不作總結了,須要的讀者能夠點擊目錄跳轉至每個小結的總結~.

參考文章

《Redis 的設計與實現(第二版)》

《Redis 深度歷險:核心原理和應用實踐》


完。

聯繫我

最後,歡迎關注個人我的公衆號【 呼延十 】,會不按期更新不少後端工程師的學習筆記。 也歡迎直接公衆號私信或者郵箱聯繫我,必定知無不言,言無不盡。


以上皆爲我的所思所得,若有錯誤歡迎評論區指正。

歡迎轉載,煩請署名並保留原文連接。

聯繫郵箱:huyanshi2580@gmail.com

更多學習筆記見我的博客或關注微信公衆號 < 呼延十 >------>呼延十

相關文章
相關標籤/搜索