Redis 底層數據結構及在類型中的使用

1. 底層數據結構

1.1 簡單動態字符串(sds)

在這裏插入圖片描述

struct sdshdr{
     // 記錄buf數組中已使用字節的數量
     // 等於 SDS 保存字符串的長度
     int len;
     // 記錄 buf 數組中未使用字節的數量
     int free;
     // 字節數組,用於保存字符串
     char buf[];
}

我們看上面對於 SDS 數據類型的定義:

  1. len 保存了SDS保存字符串的長度
  2. buf[] 數組用來保存字符串的每個元素
  3. free j記錄了 buf 數組中未使用的字節數量

sds數據結構中定義了buf(字節數組,保存字符串元素),len(記錄字節數組buf中已使用的字節數),free(記錄字節數組buf中未使用的字節數)

相比C字符串優勢

  1. 常數複雜度獲取字符串長度
    C 語言,獲取字符串的長度通常是經過遍歷計數來實現的,時間複雜度爲 O(n);
    sds有len 屬性的存在,我們獲取 SDS 字符串的長度只需要讀取 len 屬性
  2. 杜絕緩衝區溢出
    C 語言中使用 strcat 函數來進行兩個字符串的拼接,一旦沒有分配足夠長度的內存空間,就會造成緩衝區溢出
    SDS 數據類型,在進行字符修改的時候,會首先根據記錄的 len 屬性檢查內存空間是否滿足需求,如果不滿足,會進行相應的空間擴展,然後在進行修改操作,所以不會出現緩衝區溢出。
  3. 減少修改字符串的內存重新分配次數
    C語言由於不記錄字符串的長度,所以如果要修改字符串,必須要重新分配內存(先釋放再申請),因爲如果沒有重新分配,字符串長度增大時會造成內存緩衝區溢出,字符串長度減小時會造成內存泄露。
    對於SDS,由於len屬性和free屬性的存在,對於修改字符串SDS實現了空間預分配和惰性空間釋放兩種策略:
    1、空間預分配(優化字符串增長):
    sds在進行修改,還會爲sds分配額外的未使用空間。
    當len小於1mb,那麼分配和len同樣大小的未使用空間。
    當len大於1mb,那麼分配1mb未使用空間。
    通過空間預分配,redis可以減少連續執行字符串增長操作所需的內存重分配。
    2、惰性空間釋放(優化字符串縮短):
    sds要縮短保存的字符串時,不會立即使用內存重分配回收縮短出來的字節,而是使用free屬性將這些字節數量記錄下來,並等待將來使用。
    避免縮短字符串時所需的內存重分配操作,併爲將來的增長提供優化。
    當然SDS也提供了相應的API,當我們有需要時,也可以手動釋放這些未使用的空間。
  4. 二進制安全
    C字符串以空字符作爲字符串結束的標識,而對於一些二進制文件(如圖片等),內容可能包括空字符串,因此C字符串無法正確存取 SDS 的API 都是以處理二進制的方式來處理 buf 裏面的元素,並且 SDS 不是以空字符串來判斷是否結束,而是以 len 屬性表示的長度來判斷字符串是否結束。
  5. 兼容部分 C 字符串函數
    SDS 是二進制安全的,但是一樣遵從每個字符串都是以空字符串結尾的慣例,這樣可以重用 C 語言庫<string.h> 中的一部分函數。

一般來說,SDS 除了保存數據庫中的字符串值以外,SDS 還可以作爲緩衝區(buffer):包括 AOF 模塊中的AOF緩衝區以及客戶端狀態中的輸入緩衝區。
在這裏插入圖片描述

1.2 字典

類似HashMap
在這裏插入圖片描述

漸近式 rehash
單線程漸進式rehash。也就是說擴容和收縮操作不是一次性、集中式完成的,而是分多次、漸進式完成的。如果保存在Redis中的鍵值對只有幾個幾十個,那麼 rehash 操作可以瞬間完成,但是如果鍵值對有幾百萬,幾千萬甚至幾億,那麼要一次性的進行 rehash,勢必會造成Redis一段時間內不能進行別的操作。所以Redis採用漸進式 rehash,這樣在進行漸進式rehash期間,字典的刪除查找更新等操作可能會在兩個哈希表上進行,第一個哈希表沒有找到,就會去第二個哈希表上進行查找。但是進行 增加操作,一定是在新的哈希表上進行的。

1.3 跳躍表(SkipList)

跳躍表有很多層組成,每層都是鏈表,並且是有序的,每個節點保存這指向同層下一個節點和執行下一層同一個節點的指針。
最底層的鏈表保存了所有的數據。
通過分層優化搜索存儲在最底層的數據,進行搜索和插入。
插入的時候通過投硬幣的方式,判斷該節點是否可以升到上一層,每層最少有兩個節點。投硬幣的方式保證了分層足夠隨機.
刪除的時候,會將所有分層的該節點刪除。
redis使用跳錶不用b+樹,是因爲跳錶相比b+樹實現簡單,佔用內存小。

  1. 搜索:從最高層的鏈表節點開始,如果比當前節點要大和比當前層的下一個節點要小,那麼則往下找,也就是和當前層的下一層的節點的下一個節點進行比較,以此類推,一直找到最底層的最後一個節點,如果找到則返回,反之則返回空。
  2. 插入:首先確定插入的層數,有一種方法是假設拋一枚硬幣,如果是正面就累加,直到遇見反面爲止,最後記錄正面的次數作爲插入的層數。當確定插入的層數k後,則需要將新元素插入到從底層到k層。
  3. 刪除:在各個層中找到包含指定值的節點,然後將節點從鏈表中刪除即可,如果刪除以後只剩下頭尾兩個節點,則刪除這一層。

基於有序鏈表的擴展。增加了50%的存儲空間,但是性能提升了一倍。同一層只有兩個節點。
具有如下性質:

  1. 由很多層結構組成;
  2. 每一層都是一個有序的鏈表,排列順序爲由高層到底層,都至少包含兩個鏈表節點,分別是前面的head節點和後面的nil節點;
  3. 最底層的鏈表包含了所有的元素;
  4. 如果一個元素出現在某一層的鏈表中,那麼在該層之下的鏈表也全都會出現(上一層的元素是當前層的元素的子集);
  5. 鏈表中的每個節點都包含兩個指針,一個指向同一層的下一個鏈表節點,另一個指向下一層的同一個鏈表節點;
    在這裏插入圖片描述

插入
使用投硬幣方式提拔節點到上一層。
在這裏插入圖片描述

  1. 新節點和各層索引節點逐一比較,確定原鏈表的插入位置。O(logN)
  2. 把索引插入到原鏈表。O(1)
  3. 利用拋硬幣的隨機方式,決定新節點是否提升爲上一級索引。結果爲“正”則提升並繼續拋硬幣,結果爲“負”則停止。O(logN)

總體上,跳躍表插入操作的時間複雜度是O(logN),而這種數據結構所佔空間是2N,既空間複雜度是 O(N)。

刪除
在這裏插入圖片描述

  1. 自上而下,查找第一次出現節點的索引,並逐層找到每一層對應的節點。O(logN)
  2. 刪除每一層查找到的節點,如果該層只剩下1個節點,刪除整個一層(原鏈表除外)。O(logN)

總體上,跳躍表刪除操作的時間複雜度是O(logN)。

Redis用跳躍表,爲何不用B+tree?
跳躍表實現較B+tree簡單,佔用內存小。

1.4 整數集合

當一個集合中只包含整數,並且元素的個數不是很多的話,redis 會用整數集合作爲底層存儲,它的一個優點就是可以節省很多內存,雖然字典結構的效率很高,但是它的實現結構相對複雜並且會分配較多的內存空間。

通過二分查找整數判斷是否已存在。

1.5壓縮列表(ZipList)

縮列表用於元素個數少、元素長度小的場景。其優勢在於集中存儲,節省空間。

類似數組,但是數組每個元素固定大小。
壓縮列表每個元素不固定大小,並且會記錄元素的大小。

壓縮列表(zip1ist)是列表和哈希的底層實現之一。

當一個列表只包含少量列表項,並且每個列表項要麼就是小整數值,要麼就是長度比較短的字符串,那麼Redis就會使用壓縮列表來做列表的底層實現。

當一個哈希只包含少量鍵值對,比且每個鍵值對的鍵和值要麼就是小整數值,要麼就是長度比較短的字符串,那麼Redis就會使用壓縮列表來做哈希的底層實現。

2. 在類型中的使用

判斷對象類型
type key

查看值對象的編碼
object encoding key

2.1 string

整數值,embstr編碼的sds,sds

  1. int 編碼:保存的是可以用 long 類型表示的整數值。
  2. embstr 編碼:保存長度小於44字節的字符串(redis3.2版本之前是39字節,之後是44字節)。
  3. raw 編碼:保存長度大於44字節的字符串(redis3.2版本之前是39字節,之後是44字節)。

當 int 編碼保存的值不再是整數,或大小超過了long的範圍時,自動轉化爲raw。

對於 embstr 編碼,由於 Redis 沒有對其編寫任何的修改程序(embstr 是隻讀的),在對embstr對象進行修改時,都會先轉化爲raw再進行修改,因此,只要是修改embstr對象,修改後的對象一定是raw的,無論是否達到了44個字節。

2.2 list

ziplist,雙端鏈表

當同時滿足下面兩個條件時,使用ziplist(壓縮列表)編碼:

  1. 列表保存元素個數小於512個
  2. 每個元素長度小於64字節

不能滿足這兩個條件的時候使用 linkedlist 編碼。
上面兩個條件可以在redis.conf 配置文件中的 list-max-ziplist-value選項和 list-max-ziplist-entries 選項進行配置。

2.3 hash

ziplist,hash

當同時滿足下面兩個條件時,使用ziplist(壓縮列表)編碼:

  1. 列表保存元素個數小於512個
  2. 每個元素長度小於64字節

不能滿足這兩個條件的時候使用 hashtable 編碼。第一個條件可以通過配置文件中的 set-max-intset-entries 進行修改。

2.4 set

整數集合,hash

當集合同時滿足以下兩個條件時,使用 intset 編碼:

  1. 集合對象中所有元素都是整數
  2. 集合對象所有元素數量不超過512

不能滿足這兩個條件的就使用 hashtable 編碼。第二個條件可以通過配置文件的 set-max-intset-entries 進行配置。

2.5 zset

ziplist,skiplist+hash

當有序集合對象同時滿足以下兩個條件時,對象使用 ziplist 編碼:

  1. 保存的元素數量小於128;
  2. 保存的所有元素長度都小於64字節。

不能滿足上面兩個條件的使用 skiplist 編碼。以上兩個條件也可以通過Redis配置文件zset-max-ziplist-entries 選項和 zset-max-ziplist-value 進行修改。

2.6 總結

對於string 數據類型,因爲string 類型是二進制安全的,可以用來存放圖片,視頻等內容,另外由於Redis的高性能讀寫功能,而string類型的value也可以是數字,可以用作計數器(INCR,DECR),比如分佈式環境中統計系統的在線人數,秒殺等。
對於 hash 數據類型,value 存放的是鍵值對,比如可以做單點登錄存放用戶信息。
對於 list 數據類型,可以實現簡單的消息隊列,另外可以利用lrange命令,做基於redis的分頁功能
對於 set 數據類型,由於底層是字典實現的,查找元素特別快,另外set 數據類型不允許重複,利用這兩個特性我們可以進行全局去重,比如在用戶註冊模塊,判斷用戶名是否註冊;另外就是利用交集、並集、差集等操作,可以計算共同喜好,全部的喜好,自己獨有的喜好等功能。
對於 zset 數據類型,有序的集合,可以做範圍查找,排行榜應用,取 TOP N 操作等。

參考:
漫畫:什麼是跳躍表?
Redis數據結構——壓縮列表
Redis詳解(四)------ redis的底層數據結構
Redis詳解(五)------ redis的五大數據類型實現原理

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