Redis常見面試題:ZSet底層資料結構,SDS、壓縮列表ZipList、跳錶SkipList

BJRA發表於2024-11-03

文章目錄

一、Redis資料結構概述

  • 1.1 Redis有哪些資料型別
  • 1.2 Redis本質是雜湊表
  • 1.3 Redis的雜湊衝突與漸進式rehash
  • 1.4 資料結構底層
    • 1.4.1 簡單動態字串SDS
    • 1.4.2 雙向連結串列LinkedList(後續已廢棄)
    • 1.4.3 壓縮列表ZipList
    • 1.4.4 雜湊表HashTable
    • 1.4.5 跳錶SkipList
    • 1.4.6 整數陣列IntSet
    • 1.4.7 RedisObject
    • 1.4.8 Redis的編碼方式

二、String型別

三、List列表型別

  • 3.1 簡介
  • 3.2 資料結構
  • 3.3 壓縮列表ZipList
  • 3.4 雙向連結串列LinkedList(後續已廢棄)
  • 3.5 快速連結串列QuickList

四、Set集合型別

五、Hash雜湊型別

  • 5.1 簡介
  • 5.2 資料結構

六、ZSet型別

  • 6.1 簡介
  • 6.2 什麼時候採用壓縮列表、什麼時候採用跳錶
  • 6.3 跳錶
    • 6.3.1 跳錶是什麼(what)
    • 6.3.2 跳錶怎麼做的(how)
    • 6.3.3 為什麼需要跳錶(WHY)
    • 6.3.4 為什麼用跳錶而不用紅黑樹或者二叉樹呢
    • 6.3.5 zset為什麼用跳錶而不用二叉樹或者紅黑樹呢,MySQL為什麼不用跳錶

七、Stream型別

還記得Redis五種資料型別、String、List、Set、Hash、ZSet嗎?如果忘記可以到這裡重新溫習:Redis基礎(超詳解)一 :Redis定義、SQL與NoSQL區別、Redis常用命令、Redis五種資料型別、String、List、Set、Hash、ZSet;Redis的Java客戶端

Redis 是一款開源的,記憶體中的資料結構儲存系統,它可以用作資料庫、快取和訊息代理。Redis 支援多種型別的資料結構,如字串(String)、雜湊(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)、點陣圖(Bitmap)、HyperLogLog 和地理空間索引(Geospatial)。這些資料結構提供了豐富的操作,使得 Redis 能夠應對各種各樣的場景。本文將詳細介紹 Redis 的各種資料結構,包括它們的特性、底層實現、常用命令以及應用場景。

一、Redis資料結構概述

1.1 Redis有哪些資料型別

Redis是一個key-value的資料庫,key一般是String型別,不過value的型別多種多樣:包含 6 種基本型別——String(字串)、List(列表)、Hash(雜湊)、Set(集合)、Sorted Set(有序集合)、Stream(流、Redis5.0引入),和三種特殊型別——geospatial(地理位置)、Bitmap(位儲存)、HyperLog(基數統計)。

資料結構的底層實現:底層資料結構一共有 6 種,分別是簡單動態字串、雙向連結串列、壓縮列表、雜湊表、跳錶和整數陣列。

從上圖可以看出:String 的底層是簡單動態字串;List 的底層是雙向連結串列和壓縮連結串列;Set 的底層是整數陣列和雜湊表;Hash 的底層是壓縮連結串列和雜湊表;Sorted Set 底層壓縮連結串列和跳錶。即 String 型別的底層實現只有一種資料結構,也就是簡單動態字串。而 List、Set、Hash 和 Sorted Set這四種資料型別,都有兩種底層實現結構。通常情況下,我們會把這四種型別稱為集合型別,它們的特點是一個鍵對應了一個集合的資料。

Redis 之所以採用不同的資料結構,其實是在效能和記憶體使用效率之間的平衡。

1.2 Redis本質是雜湊表

Redis 本身是一個鍵值對資料庫,這種鍵值對的儲存方式就是雜湊對映(Hashmap)的一種體現,即透過鍵(Key)來快速查詢對應的值(Value)。

  • 一個雜湊表,其實就是一個陣列,陣列的每個元素稱為一個雜湊桶。也就是說,一個雜湊表是由多個雜湊桶組成的,每個雜湊桶中儲存了鍵值對資料;
  • 不管是鍵型別還是值型別,雜湊桶中的元素儲存的都不是值本身,而是指向具體值的指標

根據下圖可看出,雜湊桶中的 entry 元素中儲存了 ‘*key’ 和 '*value ’ 指標,分別指向了實際的鍵和值,這樣一來,即使值是一個集合,也可以透過 '*value 指標被查詢到:

因為這個雜湊表儲存了所有的鍵值對,所以它也叫做全域性雜湊表。

  • 雜湊表的最大好處是可以用 O(1) 的時間複雜度來快速查詢到鍵值對——我們只需要計算鍵的雜湊值,就可以知道它對應的雜湊桶位置,然後就可以訪問相應的 Entry元素;

  • 這個查詢過程主要依賴於雜湊計算,和資料量的多少並沒有直接關係。也就是說,不管雜湊表裡有 10 萬個鍵還是 100 萬個鍵,我們只需要一次計算就能找到相應的鍵。

也就是說,整個資料庫就是一個全域性 Hash 表,而 Hash 表的時間複雜度就是 O(1),只需要計算每個鍵的 Hash 值,就知道對應的 Hash 桶位置,定位桶裡面的 Entry 找到對應資料,這個也是 Redis 快的原因之一。

但如果我們只是瞭解雜湊表 O(1) 複雜度和快速查詢特性,那麼當我們向 Redis 中寫入大量資料之後,就可能發現 操作有時候會突然變慢了。原因是雜湊表的衝突問題和 rehash 可能帶來的操作阻塞。

1.3 Redis的雜湊衝突與漸進式rehash

Redis 使用雜湊表作為其底層資料結構,雜湊衝突是雜湊表中常見的問題。當兩個或更多的鍵被雜湊函式對映到同一個雜湊桶時,就會發生雜湊衝突。Redis 透過鏈地址法來解決雜湊衝突,即在每個雜湊桶中維護一個連結串列,所有雜湊到同一個桶的鍵值對都儲存在這個連結串列中。

當雜湊表中的元素數量增長到一定程度,或者雜湊表中的元素數量減少到一定程度,Redis 會觸發雜湊表的擴容或收縮,這個過程稱為 rehash。為了避免 rehash 過程中一次性複製所有元素導致的長時間阻塞,Redis 使用了一種稱為“漸進式 rehash”的策略。

在漸進式 rehash 過程中,Redis 會同時維護新舊兩個雜湊表,並在每次對雜湊表進行操作時,將一部分桶從舊雜湊表移動到新雜湊表。同時,為了保證查詢操作的正確性,Redis 在查詢時會同時查詢新舊兩個雜湊表。這樣,透過分攤在一段時間內完成 rehash,避免了一次性操作帶來的效能問題。

1.4 資料結構底層

1.4.1 簡單動態字串SDS

我們都知道Redis中儲存的Key是字串,value往往是字串或者字串的集合。可見字串是Redis中最常用的一種
資料結構。

不過Redis沒有直接使用C語言中的字串,因為C語言字串存在很多問題。在C語言中定義字串 char *str = "message",本質是字元陣列 {'m', 'e', 's', 's', 'a', 'g', 'e', '\0'}。它會儲存如下圖的樣子:

以“\0”代表結束。這樣就會產生以下問題:

  1. 無法儲存“\0”這種特殊字元,因為“\0”代表結束(非二進位制安全);
  2. 每次字串擴容和縮容,都需要使用新的char陣列;
  3. 沒有記錄字串的長度,每次都需要進行遍歷到結束、或者透過運算 才能知道長度。
  4. 不可修改

Redis構建了一種新的字串結構,稱為簡單動態字串Simple Dynamic String),簡稱SDS。(Redis是C語言實現的)

例如我們執行命令:set name 大哥,那麼Redis將在底層建立兩個SDS,其中一個是包含“name”的SDS,另一個是包含“大哥”的SDS。

SDS優點

  • 獲取字串長度的時間複雜度為O(1)

  • 支援動態擴容

  • 減少記憶體分配次數

  • 二進位制安全。遍歷字串時不是以介紹標識為標記、而是以長度為基準,

  • len:目前已使用的長度;

  • alloc:buf的總長度,就是已經分配空間的長度;

  • flags:sds的型別,用低三位標識,高5位暫時不用。sdshdr5這種型別該欄位為空;sdshdr8、sdshdr16、sdshdr32、sdshdr64用標識進行表示。

  • buf:儲存具體的字串。注意這裡也會以\0結尾,但它不會計算在len中。

1.4.2 雙向連結串列LinkedList(後續已廢棄)

LinkedList 是標準的雙向連結串列,Node 節點包含 prev 和 next 指標,分別指向後繼與前驅節點,因此從雙向連結串列中的任意一個節點開始都可以很方便地訪問其前驅與後繼節點。

LinkedList 可以進行雙向遍歷;新增刪除元素快 O(1),查詢元素慢 O(n),高效實現了 LPUSH 、RPOP、RPOPLPUSH,但由於需要為每個節點分配額外的記憶體空間,所以會浪費一定的記憶體空間。這種編碼方式適用於元素數量較多或者元素較大的場景。

LinkedList 結構為連結串列提供了表頭指標 head、表尾指標 tail,以及節點數量計算 len。下圖展示一個由 list 結構和三個 listNode 節點組成的連結串列:

Redis 的連結串列實現的特性可以總結如下:

  • 雙端:連結串列節點帶有 prev 和 next 指標,獲取某個節點的前一節點和後一節點的複雜度都是 O(1);
  • 無環:表頭節點的 prev 指標和表尾節點的 next 指標都指向 NULL,對連結串列的訪問以 NULL 為終點;
  • 表頭指標/表尾指標:透過 list 結構的 head 指標和 tail 指標,獲取連結串列的表頭節點和表尾節點的複雜度為 O(1);
  • 連結串列長度計數器:透過 list 結構的 len 屬性來對 list 的連結串列節點進行計數,獲取節點數量的複雜度為O(1);
  • 多型:連結串列節點使用 void* 指標來儲存節點值,並透過 list 結構的 dup、free、match 三個屬性為節點值設定型別特定函式,所以連結串列可以用於儲存各種不同型別的值。
  • 使用連結串列的附加空間相對太高,因為 64bit 系統中指標是 8 個位元組,所以 prev 和 next 指標需要佔據 16 個位元組,且連結串列節點在記憶體中單獨分配,會加劇記憶體的碎片化,影響記憶體管理效率

1.4.3 壓縮列表ZipList

ZipList是一種特殊的“雙端連結串列”(並非連結串列),由一系列特殊編碼的連續記憶體塊組成,像記憶體連續的陣列。可以在任意一端進行壓入/彈出操作,並且該操作的時間複雜度為O(1)。

壓縮列表 底層資料結構:本質是一個陣列,增加了列表長度、尾部偏移量、列表元素個數、以及列表結束標識,有利於快速尋找列表的首尾節點;但對於其他正常的元素,如元素2、元素3,只能一個個遍歷,效率仍沒有很高效。

屬性 型別 長度 說明
zlbytes uint32_t 4位元組 一個 4 位元組的整數,表示整個壓縮列表佔用的位元組數量,包括 <zlbytes> 自身的大小
zltail uint32_t 4位元組 一個 4 位元組的整數,記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少位元組,透過這個偏移量,可以確定表尾節點的地址
zllen uint16_t 2位元組 一個 2 位元組的整數,表示壓縮列表中的節點數量。最大值為UINT16_MAX(65534),如果超過這個數,此處會記錄為65535,但節點的真實數量需要遍歷整個壓縮列表才能計算出
entry 列表節點 不定 壓縮列表中的元素,每個元素都由一個或多個位元組組成,節點的長度由節點儲存的內容決定。每個元素的第一個位元組(又稱為"entry header")用於表示這個元素的長度以及編碼方式
zlend uint8_t 1位元組 一個位元組,特殊值0xFF(十進位制255),表示壓縮列表的結束

注意:

  • 如果查詢定位首個元素或最後1個元素,可以透過表頭 “zlbytes”、“zltail_offset” 元素快速獲取,複雜度是 O(1)。但是查詢其他元素時,就沒有這麼高效了,只能逐個查詢下去,比如 entryN 的複雜度就是 O(N)

  • ZipList雖然節省記憶體,但申請記憶體必須是連續空間,如果記憶體佔用較多,申請效率較低。

1.4.4 雜湊表HashTable

Redis 的雜湊表(hashtable)是一種常見的鍵值對對映結構,它透過一個雜湊函式將鍵對映到一個桶中,然後在桶中進行查詢。Redis 的雜湊表使用連結串列法解決雜湊衝突,即當多個鍵對映到同一個桶時,將它們儲存在同一個連結串列中。

在Redis的原始碼中,雜湊表的結構定義如下:

typedef struct dictEntry {
    void *key;
    void *val;
    struct dictEntry *next;
} dictEntry;

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dict {
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;

其中:

  • dictEntry 結構體表示雜湊表中的一個節點,包含鍵(key)、值(val)和指向下一個節點的指標(next)。

  • dictht 結構體表示一個雜湊表,包含指向雜湊表陣列的指標(table)、雜湊表陣列的大小(size)、雜湊表陣列大小掩碼(sizemask)和已使用的節點數量(used)。

  • dict 結構體表示一個字典,包含兩個雜湊表(ht)、當前進行 rehash 的索引(rehashidx)和當前執行的迭代器數量(iterators)。

1.4.5 跳錶SkipList

**SkipList(跳錶)**首先是連結串列,在連結串列的基礎上,增加了多級索引,透過多級索引位置的轉跳,實現了快速查詢元素。但與傳統連結串列相比有幾點差異:

  • 元素按照升序排列儲存
  • 節點可能包含多個指標,指標跨度不同

普通連結串列想查詢元素27,只能從連結串列頭部一個個往後遍歷,需要遍歷6次 才能找到元素27

跳錶怎麼做的(how):建立多級索引

如建立一級索引

如果覺得慢,可以建立二級索引

當資料量特別大的時候,跳錶的時間複雜度為 O(logN)。其本身利用的思想,有點類似於二分法。

SkipList的特點

  • 跳躍表是一個雙向連結串列,每個節點都包含score和ele值
  • 節點按照score值排序,score值一樣則按照ele字典排序
  • 每個節點都可以包含多層指標,層數是1到32之間的隨機數
  • 不同層指標到下一個節點的跨度不同,層級越高,跨度越大
  • 增刪改查效率與紅黑樹基本一致,實現卻更簡單

1.4.6 整數陣列IntSet

IntSet是Redis中set集合的一種實現方式,基於整數陣列來實現,並且具備長度可變、有序等特徵。

(int8_t int表示整數、8表示8bit位,即1個位元組)

為了方便查詢,Redis會將intset中所有的整數按照升序依次儲存在contents陣列中,結構如圖:

IntSet升級

現在,假設有一個intSet,元素為{5,10,20},採用的編碼是INTSET_ENC_INT16,則每個整數佔2位元組:

我們向該其中新增一個數字:50000,這個數字超出了int16_t的範圍,intset會自動升級編碼方式到合適的大小。以當前案例來說流程如下:

  1. 升級編碼為INTSET_ENC_INT32,每個整數佔4位元組,並按照新的編碼方式及元素個數擴容陣列
  2. 倒序依次將陣列中的元素複製到擴容後的正確位置
  3. 將待新增的元素放入陣列末尾
  4. 最後,將inset的encoding屬性改為INTSET_ENC_INT32,將length屬性改為4

1.4.7 RedisObject

Redis中的任意資料型別的鍵和值都會被封裝為一個RedisObject,也叫做Redis物件,原始碼如下:

1.4.8 Redis的編碼方式

Redis中會根據儲存的資料型別不同,選擇不同的編碼方式,共包含11種不同型別:

編號 編碼方式 說明
0 OBJ_ENCODING_RAW raw編碼動態字串
1 OBJ_ENCODING_INT long型別的整數的字串
2 OBJ_ENCODING_HT hash表(字典dict)
3 OBJ_ENCODING_ZIPMAP 已廢棄
4 OBJ_ENCODING_LINKEDLIST 雙端連結串列(後續已廢棄)
5 OBJ_ENCODING_ZIPLIST 壓縮列表
6 OBJ_ENCODING_INTSET 整數集合
7 OBJ_ENCODING_SKIPLIST 跳錶
8 OBJ_ENCODING_EMBSTR embstr的動態字串
9 OBJ_ENCODING_QUICKLIST 快速列表
10 OBJ_ENCODING_STREAM Stream流

五種資料結構

Redis中會根據儲存的資料型別不同,選擇不同的編碼方式。每種資料型別的使用的編碼方式如下:

資料型別 編碼方式
OBJ_STRING int、embstr、raw
OBJ_LIST LinkedList和ZipList(3.2以前)、QuickList(3.2以後)
OBJ_SET intset、HT
OBJ_ZSET ZipList、HT、SkipList
OBJ_HASH ZipList、HT

二、String型別

詳細介紹:Redis五種資料型別、String、List、Set、Hash、ZSet

String 是 Redis 最簡單的資料型別,也是最常用的資料型別。它可以包含任何資料,包括字串、整數或者浮點數。在 Redis 中,字串的最大長度可以達到 512MB。

應用場景

  • 快取:將查詢結果、頁面內容等快取在 Redis 的 String 結構中,提高系統訪問速度。
  • 計數器:Redis String 結構可以將字串解析為整數進行自增或自減操作,適合做各種計數器。
  • 分散式鎖:利用 Redis String 結構的原子性操作,可以實現分散式鎖。

底層結構:Redis 的 String 型別是二進位制安全的,基於簡單動態字串(SDS)實現,它的底層實際上是一個位元組陣列,因此 String 型別可以包含任何資料,例如 jpg 圖片或者序列化的物件。

在Redis實現中並沒有直接採用C語言的字串,而是自定義了一個SDS(簡單動態字串)的結構體來標識字串。

在C語言中定義字串 char *str = "message"它會儲存如下圖的樣子:

以“\0”代表結束。這樣就會產生以下問題:

  1. 無法儲存“\0”這種特殊字元,因為“\0”代表結束;
  2. 每次字串擴容和縮容,都需要使用新的char陣列;
  3. 沒有記錄字串的長度,每次都需要進行遍歷到結束才能知道長度。

redis要解決上述問題,就自定義了一個SDS結構,如下圖:

  • len:目前已使用的長度;
  • alloc:buf的總長度,就是已經分配空間的長度;
  • flags:sds的型別,用低三位標識,高5位暫時不用。sdshdr5這種型別該欄位為空;sdshdr8、sdshdr16、sdshdr32、sdshdr64用標識進行表示。
  • buf:儲存具體的字串。注意這裡也會以\0結尾,但它不會計算在len中。

redis可以根據字串的大小使用不同型別的sds,這樣可以進一步的節省記憶體。這裡不需要擔心buf的長度不夠用,2的64次冪是一個非常巨大的數字,同時redis預設也會限制最大的字元為512M,在6.3版本開始可以對最大限制字元大小進行配置。注意:使用不同型別的sds並不是一次性分配這麼多空間,而是逐步分配的,例如:使用sdshdr16這種型別,存入一個長度為14的字串,那麼會根據存入的字串長度預留空閒長度,這裡是14;如果字串大小超過1M,那麼預留空間就是1M。

redis除了自定義了SDS型別來儲存字串,還定義了三種編碼:

  • int:8位元組的長整形,值時數字型別,並且數字長度小於20;
  • embstr:長度小於等於44位元組的字串;(3.2版本之前是39位元組)
  • raw:長度大於44位元組的字串。

三、List列表型別

3.1 簡介

詳細介紹:Redis五種資料型別、String、List、Set、Hash、ZSet

Redis中的List型別與Java中的LinkedList類似,可以看做是一個雙向連結串列結構,按插入順序排序,你可以新增一個元素到頭部(左邊)或尾部(右邊)。既可以支援正向檢索和也可以支援反向檢索。特徵也與LinkedList類似:有序、元素可重複、插入和刪除快、查詢速度一般。在 Redis 中,列表最多可以包含 2^32 - 1 個元素。

應用場景

  • 訊息佇列:可以利用 List 的 push 和 pop 操作,實現生產者消費者模型。
  • 時間線、動態訊息:比如微博的時間線,可以將最新的內容放在 List 的最前面。
  • 常用來儲存一個有序資料,例如:朋友圈點贊列表,評論列表等

底層結構

  • 在3.2版本之前,Redis List底層採用壓縮連結串列ZipList雙向連結串列LinkedList來實現List。當元素數量小於512並且元素大小小於64位元組時採用ZipList編碼,超過則將自動採用LinkedList編碼
  • 在3.2版本之後,Redis統一採用快速連結串列QuickList來實現List

3.2 資料結構

Redis的List結構類似一個雙端連結串列,可以從首、尾操作列表中的元素:

  • 在Redis 3.2版本之前,Redis List底層採用壓縮連結串列ZipList雙向連結串列LinkedList來實現List。當元素數量小於512個並且元素大小小於64位元組時採用ZipList編碼,超過則將自動採用LinkedList編碼。
  • 在3.2版本之後,Redis統一採用快速連結串列QuickList結構來實現List

QuickList結構如下:

在 Redis3.2 版本前,Redis 列表 List 使用兩種資料結構作為底層實現:

  • 壓縮列表 ZipList:插入元素過多或字串太大,就需要呼叫 Realloc 擴充套件記憶體;
  • 雙向連結串列 LinkedList:需附加指標 Prev 和 Next,較浪費空間,加重記憶體的碎片化

3.3 壓縮列表ZipList

在 Redis3.2 版本前 壓縮列表不僅是 List 的底層實現之一,同時也是 Hash、 ZSet 兩種資料型別底層實現之一。

ZipList是一種特殊的“雙端連結串列”(並非連結串列),由一系列特殊編碼的連續記憶體塊組成,像記憶體連續的陣列。可以在任意一端進行壓入/彈出操作,並且該操作的時間複雜度為O(1)。

壓縮列表 底層資料結構:本質是一個陣列,增加了列表長度、尾部偏移量、列表元素個數、以及列表結束標識,有利於快速尋找列表的首尾節點;但對於其他正常的元素,如元素2、元素3,只能一個個遍歷,效率仍沒有很高效。

當我們的 List 列表資料量比較少的時候,且儲存的資料輕量的(如小整數值、短字串)時候, Redis 就會透過壓縮列表來進行底層實現。

屬性 型別 長度 說明
zlbytes uint32_t 4位元組 一個 4 位元組的整數,表示整個壓縮列表佔用的位元組數量,包括 <zlbytes> 自身的大小
zltail uint32_t 4位元組 一個 4 位元組的整數,記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少位元組,透過這個偏移量,可以確定表尾節點的地址
zllen uint16_t 2位元組 一個 2 位元組的整數,表示壓縮列表中的節點數量。最大值為UINT16_MAX(65534),如果超過這個數,此處會記錄為65535,但節點的真實數量需要遍歷整個壓縮列表才能計算出
entry 列表節點 不定 壓縮列表中的元素,每個元素都由一個或多個位元組組成,節點的長度由節點儲存的內容決定。每個元素的第一個位元組(又稱為"entry header")用於表示這個元素的長度以及編碼方式
zlend uint8_t 1位元組 一個位元組,特殊值0xFF(十進位制255),表示壓縮列表的結束

注意:

  • 如果查詢定位首個元素或最後1個元素,可以透過表頭 “zlbytes”、“zltail_offset” 元素快速獲取,複雜度是 O(1)。但是查詢其他元素時,就沒有這麼高效了,只能逐個查詢下去,比如 entryN 的複雜度就是 O(N)

  • ZipList雖然節省記憶體,但申請記憶體必須是連續空間,如果記憶體佔用較多,申請效率較低。

3.4 雙向連結串列LinkedList(後續已廢棄)

Redis後續版本已廢棄雙向連結串列LinkedList,有關LinkedList的細節 可查閱本文1.4.2小節。

3.5 快速連結串列QuickList

QuickList底層 LinkedList + ZipList,可以從雙端訪問,記憶體佔用較低,保含多個ZipList,儲存上限高。其特點:

  • 是一個節點為ZipList的雙端連結串列
  • 節點採用ZipList,解決了傳統連結串列的記憶體佔用問題
  • 控制了ZipList大小,解決連續記憶體空間申請效率問題
  • 中間節點可以壓縮,進一步節省了記憶體

ZipList雖然節省記憶體,但申請記憶體必須是連續空間,如果記憶體佔用較多,申請效率較低。

四、Set集合型別

詳細介紹:Redis五種資料型別、String、List、Set、Hash、ZSet

Set 是 Redis 的一種資料型別,它是字串型別的無序集合,不一定確保元素有序,可以滿足元素唯一、查詢效率要求極高。和列表一樣,你可以新增、刪除、查詢元素。但它保證每個元素只出現一次。在 Redis 中,集合最多可以包含 2^32 - 1 個元素。滿足下列特點:

  • 不保證有序性
  • 保證元素唯一(可以判斷元素是否存在)
  • 求交集、並集、差集

應用場景

  1. 社交網路中的好友關係、共同好友、二度好友等功能。
  2. 利用集合支援的交集、並集、差集等操作,可以計算共同喜好、全部的喜好、自己獨有的喜好等。

底層結構

Redis Set 的底層實現為整數集合和雜湊表兩種,當集合中的元素都是整數且元素數量較少時,Redis 會選擇整數集合作為底層實現,這樣可以更加節省記憶體;當資料量變大或者集合中的元素不全是整數時,Redis 會自動將底層實現從整數集合切換為雜湊表(類似於Java 中,hashset是基於hashmap實現的)

  • 為了查詢效率和唯一性,Set採用HT編碼(Dict)。Dict中的key用來儲存元素,Value統一為null。
  • 當儲存的所有資料都是整數,並且元素數量不超過set-max-intset-entries時,Set會採用lntSet編碼,以節省記憶體。

注意事項

  1. 集合中的元素是無序的,如果需要獲取有序的資料,可以使用 Sorted Set 資料型別。
  2. 集合中的元素不允許重複,如果需要儲存重複元素,可以使用 List 資料型別。

五、Hash雜湊型別

5.1 簡介

詳細介紹:Redis五種資料型別、String、List、Set、Hash、ZSet

Hash 是 Redis 的一種資料型別,也叫雜湊,其value是一個無序字典,類似於Python 的字典、Java中的HashMap結構。它是鍵值對集合,是一個字串欄位和字串值之間的對映表,其欄位和值的最大長度都是 512MB。在 Redis 中,雜湊可以儲存超過 4 億個鍵值對。

String結構是將物件序列化為JSON字串後儲存,當需要修改物件某個欄位時很不方便:

KEY VALUE
jw:user:1
jw:product:1

Hash結構可以將物件中的每個欄位獨立儲存,可以針對單個欄位做CRUD,只需要透過key + field找到:

Hash結構與Redis中的Zset非常類似:

  • 都是鍵值儲存
  • 都需求根據鍵獲取值
  • 鍵必須唯一

區別如下:

  • zset的鍵是member,值是score;hash的鍵和值都是任意值
  • zset要根據score排序;hash則無需排序

因此Hash底層採用的編碼與ZSet也基本一致,只需要把排序有關的SkipList去掉即可

應用場景

  • 儲存物件:Hash 結構可以看作是 String 型別的 field 和 value 的對映表,非常適合用於儲存物件。例如,你可以使用 Hash 型別儲存使用者的資訊,如使用者名稱、密碼、郵箱等;
  • 資料快取:可以將資料庫中的一條記錄對映成一個 Hash 結構,Hash 的每個欄位對應記錄的每個列;
  • 資料分析:你可以使用 Hash 型別儲存各種統計資料,例如使用者的行為資料,然後進行資料分析;
  • 社交網路:在社交網路應用中,你可以使用 Hash 型別儲存使用者的朋友列表、粉絲列表等

底層結構

Hash底層採用的編碼與ZSet也基本一致,只需要把排序有關的SkipList去掉即可。Redis Hash 的底層實現為壓縮列表和雜湊表兩種,當 Hash 中的元素個數較少且每個元素的大小較小的時候,Redis 會選擇壓縮列表作為底層實現,這樣可以更加節省記憶體;當資料量變大時,Redis 會自動將底層實現從壓縮列表切換為雜湊表。

  • Hash結構預設採用ZipList編碼,用以節省記憶體。ZipList中相鄰的兩個entry分別儲存field和value
  • 當資料量較大時,Hash結構會轉為HT編碼,也就是Dict,觸發條件有兩個:
    • ZipList中的元素數量超過了hash-max-ziplist-entries(預設512)
    • ZipList中的任意entry大小超過了hash-max-ziplist-value(預設64位元組)

5.2 資料結構

Redis 的 Hash 型別的底層實現是一個非常最佳化的資料結構,它會根據實際情況選擇使用緊湊的**壓縮列表(ziplist)或者雜湊表(hashtable)**作為底層實現。

  • Hash結構預設採用ZipList編碼,用以節省記憶體。ZipList中相鄰的兩個entry分別儲存field和value
  • 當資料量較大時,Hash結構會轉為HT編碼,也就是Dict,觸發條件有兩個:
    • ZipList中的元素數量超過了hash-max-ziplist-entries(預設512)
    • ZipList中的任意entry大小超過了hash-max-ziplist-value(預設64位元組)

Redis 的 Hash 型別會根據實際情況在壓縮列表(ziplist)和雜湊表(hashtable)之間進行切換,這主要取決於兩個配置引數:hash-max-ziplist-entrieshash-max-ziplist-value

  • hash-max-ziplist-entries:這個引數用於設定壓縮列表可以儲存的最大節點數量。如果一個 Hash 型別的元素數量超過這個值,那麼就會從壓縮列表切換到雜湊表。預設值為 512;

  • hash-max-ziplist-value:這個引數用於設定壓縮列表中每個節點的最大值大小(以位元組為單位)。如果一個 Hash 型別的任何元素的大小超過這個值,那麼就會從壓縮列表切換到雜湊表。預設值為 64。

這兩個引數都可以在 Redis 的配置檔案中進行設定。透過調整這兩個引數,你可以根據自己的應用特性,選擇更傾向於節省記憶體,還是更傾向於提高效能。

  • 從壓縮列表轉換到雜湊表:當 Hash 型別儲存的欄位和值的數量超過 hash-max-ziplist-entries 的值,或者任何欄位或值的大小超過 hash-max-ziplist-value 的值時,Redis 會將底層結構從壓縮列表轉換為雜湊表。這個過程是自動進行的,對使用者來說是透明的。

  • 從雜湊表轉換到壓縮列表:一旦 Hash 型別的底層結構被轉換為雜湊表,就無法再轉換回壓縮列表。這是因為雜湊表的效能更高,而且在大多數情況下,一旦一個 Hash 型別的大小超過了一定的閾值,那麼它的大小就很可能會繼續增長。

壓縮列表(ziplist)與雜湊表(hashtable)的詳細介紹 可見本文1.4章節。

六、ZSet型別

6.1 簡介

詳細介紹:Redis五種資料型別、String、List、Set、Hash、ZSet

ZSet(有序集合)也就是SortedSet,其中每一個元素都需要指定一個score值和member值。它是一個可排序的set集合,在 Set 的基礎上增加了一個權重引數 score,使得集合中的元素能夠按 score 進行有序排列。在 Redis 中,有序集合的最大成員數是 2^32 - 1。ZSet具備下列特性:

  • 可排序。根據score值排序
  • 元素不重複,member必須唯一
  • 查詢速度快,也可以根據member查詢分數

因為ZSet的可排序特性,經常被用來實現排行榜這樣的功能。

應用場景

  • 排行榜應用:有序集合使得我們能夠方便地實現排行榜,比如網站的文章排行、學生成績排行等。
  • 帶權重的訊息佇列:可以透過 score 來控制訊息的優先順序。

底層結構

ZSet與Java中的TreeSet有些類似,但底層資料結構卻差別很大。ZSet中的每一個元素都帶有一個score屬性,可以基於score屬性對元素排序,底層的實現是一個跳錶(SkipList)加 hash表。注意,集合成員是唯一的,但是評分可以重複。

  • Redis ZSet 的底層實現為跳躍列表和雜湊表兩種,跳躍列表保證了元素的排序和快速的插入效能,雜湊表則提供了快速查詢的能力。

  • 當元素數量不多時,HT和SkipList的優勢不明顯,而且更耗記憶體。因此zset還會採用ZipList結構來節省記憶體,不過需要同時滿足兩個條件:

    • 元素數量小於zset_max_ziplist_entries,預設值128
    • 每個元素都小於zset_max_ziplist_value位元組,預設值64

補充:ziplist本身沒有排序功能,而且沒有鍵值對的概念,因此需要有zset透過編碼實現:

  • ZipList是連續記憶體,因此score和element是緊挨在一起的兩個entry,element在前,score在後
  • score越小越接近隊首,score越大越接近隊尾,按照score值升序排列

注意事項

  1. 有序集合中的元素是唯一的,但分數(score)可以重複。
  2. 插入、刪除、查詢的時間複雜度都是 O(log(N))。對於獲取排名(排行榜)的操作,Redis 有序集合是非常高效的。

6.2 什麼時候採用壓縮列表、什麼時候採用跳錶

什麼時候採用壓縮列表、什麼時候採用跳錶呢

  • 有序集合儲存的元素數量小於128個
  • 有序集合儲存的所有元素的長度小於64位元組

上述 1且2的時候,採用壓縮列表;否則採用跳錶

6.3 跳錶

學習一個新知識,從三方面分析:WHAT、WHY、HOW

6.3.1 跳錶是什麼(what)

**SkipList(跳錶)**首先是連結串列,在連結串列的基礎上,增加了多級索引,透過多級索引位置的轉跳,實現了快速查詢元素。但與傳統連結串列相比有幾點差異:

  • 元素按照升序排列儲存
  • 節點可能包含多個指標,指標跨度不同

SkipList的特點

  • 跳躍表是一個雙向連結串列,每個節點都包含score和ele值
  • 節點按照score值排序,score值一樣則按照ele字典排序
  • 每個節點都可以包含多層指標,層數是1到32之間的隨機數
  • 不同層指標到下一個節點的跨度不同,層級越高,跨度越大
  • 增刪改查效率與紅黑樹基本一致,實現卻更簡單

普通連結串列想查詢元素27,只能從連結串列頭部一個個往後遍歷,需要遍歷6次 才能找到元素27

6.3.2 跳錶怎麼做的(how)

跳錶怎麼做的(how):建立多級索引

如建立一級索引

如果覺得慢,可以建立二級索引

當資料量特別大的時候,跳錶的時間複雜度為 O(logN)。其本身利用的思想,有點類似於二分法。

6.3.3 為什麼需要跳錶(WHY)

因為普通連結串列查詢一個元素 時間複雜度O(n);而跳錶查詢的時間複雜度為O(logn),查詢速度更快。不僅如此,刪除、插入等操作的時間複雜度也是O(logn)

6.3.4 為什麼用跳錶而不用紅黑樹或者二叉樹呢

紅黑樹、二叉樹查詢一個元素的時間複雜度也是O(logn)

  • ZSet有個核心操作,範圍查詢:跳錶效率比紅黑樹高,跳錶可以做到 logn 時間複雜度內,快速查詢,找到區間起點、依次往後遍歷即可,但紅黑樹範圍查詢的效率沒有跳錶高(每一層加了指標)
  • 跳錶實現比紅黑樹及平衡二叉樹簡單、易懂:可以有效控制跳錶的索引層級來控制記憶體的消耗,

6.3.5 zset為什麼用跳錶而不用二叉樹或者紅黑樹呢,MySQL為什麼不用跳錶

Redis是直接操作記憶體的、並不需要磁碟io,而MySQL需要去讀取磁碟io,所以MySQL使用b+樹的方式去減少磁碟io。B+樹原理是 葉子節點儲存資料、非葉子節點儲存索引,每次讀取磁碟頁時就會讀取一整個節點,每個葉子節點還要指向前後節點的指標,其目的是最大限度地降低磁碟io

資料在記憶體種讀取 耗費的時間是磁碟IO讀取的百萬分之一,而Redis是記憶體中讀取資料、不涉及IO,因此使用了跳錶,跳錶模型是更快更簡單的方式

七、Stream型別

Stream 是 Redis 5.0 版本引入的新特性,它是一種類似於日誌系統的資料結構,用於儲存多個鍵值對的列表,每個鍵值對都會被分配一個自動遞增的ID。Stream 主要用於實現訊息佇列的功能,如 Apache Kafka。

應用場景

  • 訊息佇列:Stream 可以作為生產者消費者模型的一種實現,生產者新增訊息到 Stream,消費者從 Stream 中讀取訊息並處理。
  • 日誌記錄:由於 Stream 中的每個元素都有唯一的 ID,並且這個 ID 是自動遞增的,因此非常適合用來記錄日誌。

底層結構

Redis Stream 的底層實現為一種叫做快速列表(quicklist)的資料結構,這是一種同時包含了壓縮列表(ziplist)和雙向連結串列特性的資料結構,既可以利用壓縮列表節省記憶體,又可以利用雙向連結串列在兩端進行快速的新增、刪除操作。

常用命令

XADD key ID field value [field value …]:向 Stream 中新增元素。
XRANGE key start end [COUNT count]:獲取 Stream 中指定範圍的元素。
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key …] ID [ID …]:從 Stream 中讀取資料。
XDEL key ID [ID …]:從 Stream 中刪除指定 ID 的元素。
XLEN key:獲取 Stream 中的元素數量。

注意事項

  • Stream 是 Redis 中唯一一個可以安全地進行多個寫入操作的資料結構,因為每個元素都有一個唯一的、自動遞增的 ID。
  • Stream 中的元素一旦被新增,就不能被修改,只能被刪除。

參考 Redis資料結構:Hash型別全面解析Redis資料結構:List型別全面解析

相關文章