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

前言

Redis 已經是大家耳熟能詳的東西了,日常工作也都在使用,面試中也是高頻的會涉及到,那麼我們對它究竟瞭解有多深刻呢?

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

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

定義

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

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

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

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

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

字符串對象

涉及到的數據結構,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 深度歷險:核心原理和應用實踐》


完。

聯繫我

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


以上皆爲個人所思所得,如有錯誤歡迎評論區指正。

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

聯繫郵箱:[email protected]

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

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