前言
Redis 已經是大家耳熟能詳的東西了,日常工作也都在使用,面試中也是高頻的會涉及到,那麼我們對它究竟瞭解有多深刻呢?
我讀了幾本 Redis 相關的書籍,嘗試去了解它的具體實現,將一些底層的數據結構及實現原理記錄下來。
本文將介紹 Redis 中 五種基礎數據類型 的實現方法。 這五種基本類型基本覆蓋了我們業務中使用的 80%的場景,對面試也覆蓋至少 90%.(其中重點當然是有序集合以及散列結構咯).
定義
在前面的八篇文章中,我們詳細的介紹了 Redis 中的 8 種基本數據結構,但是衆所周知,Redis 常用的數據類型有五種。包括,字符串,列表,集合,有序集合,哈希。
而這五種數據類型,底層就是用前面介紹的數據結構實現的,當然,並不是直接一對一的綁定關係,而是採用了精妙的設計,構建了一個對象系統。
熟悉 OOP 編程的讀者,可能很快就能想到爲什麼要這麼設計了,對象系統帶來的好處是非常多的,但是並不在這一篇文章中講。這裏只是提到對象系統,讓大家對於五種數據類型爲什麼可以用一些花裏胡哨的方法來實現,有一個初步的瞭解。
接下來將逐一分析五種數據類型的底層實現數據結構,及實現方式(編碼)之間的切換條件。
注:後續提到五種數據類型,用 xx 對象來指代。比如 字符串對象,列表對象。提到的底層數據結構,用全稱來講。
字符串對象
涉及到的數據結構,SDS
, 強烈建議閱讀本系列第一篇文章。
字符串對象的底層實現有三種可能:int, raw, embstr.
int
如果一個字符串對象,保存的值是一個整數值,並且這個整數值在 long 的範圍內,那麼 redis 用整數值來保存這個信息,並且將字符串編碼設置爲 int
.
比如:
raw
如果字符串對象保存的是一個字符串, 並且長度大於 32 個字節,它就會使用前面講過的SDS(簡單動態字符串)
數據結構來保存這個字符串值,並且將字符串對象的編碼設置爲raw
.
embstr
如果字符串對象保存的是一個字符串, 但是長度小於 32 個字節,它就會使用embstr
來保存了,embstr
編碼不是一個數據結構,而是對 SDS 的一個小優化,當使用 SDS 的時候,程序需要調用兩次內存分配,來給 字符串對象 和 SDS 各自分配一塊空間,而embstr
只需要一次內存分配,因爲他需要的空間很少,所以採用 連續的空間保存,即將 SDS 的值和 字符串對象的值放在一塊連續的內存空間上。這樣能在短字符串的時候提高一些效率。
比如:
浮點數如何保存?
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 編碼。
intset 編碼時,底層使用 intset
數據結構。
hashtable
當元素不符合全部爲整數值且元素個數小於 512
時,集合對象使用的編碼方式爲** hashtable**.
字典的每一個鍵都是一個字符串對象,其中保存了集合裏的一個元素,字典的值全部被設置爲 NULL.
總結
編碼 | 使用條件 |
---|---|
intset | 所有元素都是整數且元素個數小於 512 |
hashtable | 其他數據 |
有序集合對象
涉及到的數據結構,壓縮列表
, 跳躍表
, 字典
, 強烈建議閱讀本系列 第三篇,第六篇,第七篇文章。
有序集合對象的編碼可以是 ziplist
以及skiplist
.
ziplist 編碼
當使用 ziplist 編碼時,有序集合對象的實現數據結構爲ziplist
(聽起來像句廢話), 每個集合的元素 (key-value), 使用兩個緊挨着的壓縮列表的節點來表示,第一個節點保存集合元素的成員 (member), 第二個節點保存集合元素的分支 (score).
在壓縮列表的內部,集合元素按照分值從小到大進行排序。
skiplist 編碼
當使用 skiplist 編碼的時候,內部使用zset
來實現數據的保存,zset
的定義如下:
typedef struct zset{
zskiplist *zsl;
dict *dict;
}zset;
爲什麼需要同時使用跳躍表以及字典呢?
其實如果我們細想,單獨使用字典或者跳躍表,都是可以實現有序集合的所有功能的,但是性能太差勁了。
- 當我們只使用字典來實現,我們可以以 O(1) 的時間複雜度獲取成員的分值,但是由於字典是無序的,當我們需要進行範圍性操作的時候,需要對字典中的所有元素進行排序,這個時間複雜度至少需要 O(nlogn).
- 當我們只使用跳躍表來實現,我們可以在 O(logn) 的時間進行範圍排序操作,但是如果要獲取到某個元素的分值,時間複雜度也是 O(logn).
因此,將字典和跳躍表結合進行使用,可以在 O(1) 的時間複雜度下完成查詢分值操作,而對一些範圍操作,使用跳躍表可以達到 O(logn) 的是纏綿複雜度。
可以看到,我在上一次的例子中,添加了一個很長的 key 之後,有序集合的編碼方式就成爲了skiplist
.
總結
編碼 | 使用條件 |
---|---|
ziplist | 元素數量少於 128 且 所有元素成員的長度小於 64 字節 |
skiplist | 不滿足上述條件的其他情況 |
散列對象
涉及到的數據結構,壓縮列表
, 字典
, 強烈建議閱讀本系列 第三篇,第六篇文章。
哈希對象的編碼可以是ziplist
或者hashtable
.
ziplist 編碼
ziplist 編碼下的哈希對象,使用了壓縮列表作爲底層實現數據結構,用兩個連續的壓縮列表節點來表示哈希對象中的一個鍵值對。實現方式類似於上面的有序集合的場景。
如圖中所示,當我放入了兩個簡單的鍵值對,此時哈希對象的編碼爲 ziplist.
hashtable 編碼
這是對 hashtable 最直觀的應用了~
哈希結構本身在結構上和字典 (hashtable) 就頗爲相似,因此哈希對象中的每一個鍵值對都是字典中的一個鍵值對。
- 字典的每一個鍵都是一個字符串對象,對象中保存了鍵值對的鍵。
- 字典的每一個值都是一個字符串對象,對象中保存了鍵值對的值。
如圖中所示,當我在上一個示例中額外加入一個很長的值,那麼編碼方式就來到了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]
更多學習筆記見個人博客或關注微信公衆號 < 呼延十 >------>呼延十