91欧美超碰AV自拍|国产成年人性爱视频免费看|亚洲 日韩 欧美一厂二区入|人人看人人爽人人操aV|丝袜美腿视频一区二区在线看|人人操人人爽人人爱|婷婷五月天超碰|97色色欧美亚州A√|另类A√无码精品一级av|欧美特级日韩特级

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Redis五種常見對象類型的底層數(shù)據(jù)結(jié)構(gòu)

數(shù)據(jù)分析與開發(fā) ? 來源:數(shù)據(jù)分析與開發(fā) ? 作者:伍陸七 ? 2020-11-14 09:50 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

Redis 是一個(gè)基于內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)系統(tǒng),可以用作數(shù)據(jù)庫、緩存和消息中間件。Redis 支持五種常見對象類型:字符串(String)、哈希(Hash)、列表(List)、集合(Set)以及有序集合(Zset),我們在日常工作中也會(huì)經(jīng)常使用它們。知其然,更要知其所以然,本文將會(huì)帶你讀懂這五種常見對象類型的底層數(shù)據(jù)結(jié)構(gòu)。

本文主要內(nèi)容參考自《Redis設(shè)計(jì)與實(shí)現(xiàn)》

1. 對象類型和編碼

Redis 使用對象來存儲(chǔ)鍵和值的,在Redis中,每個(gè)對象都由 redisObject 結(jié)構(gòu)表示。redisObject 結(jié)構(gòu)主要包含三個(gè)屬性:type、encoding 和 ptr。

typedef struct redisObject { // 類型 unsigned type:4; // 編碼 unsigned encoding:4; // 底層數(shù)據(jù)結(jié)構(gòu)的指針 void *ptr;} robj;

其中 type 屬性記錄了對象的類型。對于 Redis 來說,鍵對象總是字符串類型,值對象可以是任意支持的類型。因此,當(dāng)我們說 Redis 鍵采用哪種對象類型的時(shí)候,指的是對應(yīng)的值采用哪種對象類型。

*ptr 屬性指向了對象的底層數(shù)據(jù)結(jié)構(gòu),而這些數(shù)據(jù)結(jié)構(gòu)由 encoding 屬性決定。

之所以由 encoding 屬性來決定對象的底層數(shù)據(jù)結(jié)構(gòu),是為了實(shí)現(xiàn)同一對象類型,支持不同的底層實(shí)現(xiàn)。這樣就能在不同場景下,使用不同的底層數(shù)據(jù)結(jié)構(gòu),進(jìn)而極大提升Redis的靈活性和效率。

底層數(shù)據(jù)結(jié)構(gòu)后面會(huì)詳細(xì)講解,這里簡單看一下即可。

2. 字符串對象

字符串是我們?nèi)粘9ぷ髦杏玫米疃嗟膶ο箢愋?,它對?yīng)的編碼可以是 int、raw 和 embstr。字符串對象相關(guān)命令可參考:Redis命令-Strings。

如果一個(gè)字符串對象保存的是不超過 long 類型的整數(shù)值,此時(shí)編碼類型即為 int,其底層數(shù)據(jù)結(jié)構(gòu)直接就是 long 類型。例如執(zhí)行 set number 10086,就會(huì)創(chuàng)建 int 編碼的字符串對象作為 number 鍵的值。

如果字符串對象保存的是一個(gè)長度大于 39 字節(jié)的字符串,此時(shí)編碼類型即為 raw,其底層數(shù)據(jù)結(jié)構(gòu)是簡單動(dòng)態(tài)字符串(SDS);如果長度小于等于 39 個(gè)字節(jié),編碼類型則為 embstr,底層數(shù)據(jù)結(jié)構(gòu)就是 embstr 編碼 SDS。下面,我們詳細(xì)理解下什么是簡單動(dòng)態(tài)字符串。

2.1 簡單動(dòng)態(tài)字符串

SDS 定義

在 Redis 中,使用 sdshdr 數(shù)據(jù)結(jié)構(gòu)表示 SDS:

struct sdshdr { // 字符串長度 int len; // buf數(shù)組中未使用的字節(jié)數(shù) int free; // 字節(jié)數(shù)組,用于保存字符串 char buf[];};

SDS 遵循了 C 字符串以空字符結(jié)尾的慣例,保存空字符的 1 字節(jié)不會(huì)計(jì)算在 len 屬性里面。例如,Redis 這個(gè)字符串在 SDS 里面的數(shù)據(jù)可能是如下形式:

SDS 與 C 字符串的區(qū)別

C 語言使用長度為 N+1 的字符數(shù)組來表示長度為N的字符串,并且字符串的最后一個(gè)元素是空字符 。Redis 采用 SDS 相對于 C 字符串有如下幾個(gè)優(yōu)勢:

常數(shù)復(fù)雜度獲取字符串長度;

杜絕緩沖區(qū)溢出;

減少修改字符串時(shí)帶來的內(nèi)存重分配次數(shù);

二進(jìn)制安全。

常數(shù)復(fù)雜度獲取字符串長度

因?yàn)?C 字符串并不記錄自身的長度信息,所以為了獲取字符串的長度,必須遍歷整個(gè)字符串,時(shí)間復(fù)雜度是O(N)。而 SDS 使用 len 屬性記錄了字符串的長度,因此獲取 SDS字符串長度的時(shí)間復(fù)雜度是O(1)。

杜絕緩沖區(qū)溢出

C 字符串不記錄自身長度帶來的另一個(gè)問題是,很容易造成緩存區(qū)溢出。比如使用字符串拼接函數(shù)(stract)的時(shí)候,很容易覆蓋掉字符數(shù)組原有的數(shù)據(jù)。

與 C 字符串不同,SDS 的空間分配策略完全杜絕了發(fā)生緩存區(qū)溢出的可能性。當(dāng) SDS 進(jìn)行字符串?dāng)U充時(shí),首先會(huì)檢查當(dāng)前的字節(jié)數(shù)組的長度是否足夠。如果不夠的話,會(huì)先進(jìn)行自動(dòng)擴(kuò)容,然后再進(jìn)行字符串操作。

減少修改字符串時(shí)帶來的內(nèi)存重分配次數(shù)

因?yàn)?C 字符串的長度和底層數(shù)據(jù)是緊密關(guān)聯(lián)的,所以每次增長或者縮短一個(gè)字符串,程序都要對這個(gè)數(shù)組進(jìn)行一次內(nèi)存重分配:

如果是增長字符串操作,需要先通過內(nèi)存重分配來擴(kuò)展底層數(shù)組空間大小,不這么做就導(dǎo)致緩存區(qū)溢出;

如果是縮短字符串操作,需要先通過內(nèi)存重分配來來回收不再使用的空間,不這么做就導(dǎo)致內(nèi)存泄漏。

因?yàn)閮?nèi)存重分配涉及復(fù)雜的算法,并且可能需要執(zhí)行系統(tǒng)調(diào)用,所以通常是個(gè)比較耗時(shí)的操作。對于 Redis 來說,字符串修改是一個(gè)十分頻繁的操作。如果每次都像 C 字符串那樣進(jìn)行內(nèi)存重分配,對性能影響太大了,顯然是無法接受的。

SDS 通過空閑空間解除了字符串長度和底層數(shù)據(jù)之間的關(guān)聯(lián)。在 SDS 中,數(shù)組中可以包含未使用的字節(jié),這些字節(jié)數(shù)量由 free 屬性記錄。通過空閑空間,SDS 實(shí)現(xiàn)了空間預(yù)分配和惰性空間釋放兩種優(yōu)化策略。

1.空間預(yù)分配

空間預(yù)分配是用于優(yōu)化 SDS 字符串增長操作的,簡單來說就是當(dāng)字節(jié)數(shù)組空間不足觸發(fā)重分配的時(shí)候,總是會(huì)預(yù)留一部分空閑空間。這樣的話,就能減少連續(xù)執(zhí)行字符串增長操作時(shí)的內(nèi)存重分配次數(shù)。

有兩種預(yù)分配的策略:

len 小于 1MB 時(shí):每次重分配時(shí)會(huì)多分配同樣大小的空閑空間;

len 大于等于 1MB 時(shí):每次重分配時(shí)會(huì)多分配 1MB 大小的空閑空間。

2. 惰性空間釋放

惰性空間釋放是用于優(yōu)化 SDS 字符串縮短操作的。簡單來說就是當(dāng)字符串縮短時(shí),并不立即使用內(nèi)存重分配來回收多出來的字節(jié),而是用 free 屬性記錄,等待將來使用。SDS 也提供直接釋放未使用空間的 API,在需要的時(shí)候,也能真正的釋放掉多余的空間。

二進(jìn)制安全

C 字符串中的字符必須符合某種編碼,并且除了字符串末尾之外,其它位置不允許出現(xiàn)空字符。這些限制使得 C 字符串只能保存文本數(shù)據(jù)。

但是對于 Redis 來說,不僅僅需要保存文本,還要支持保存二進(jìn)制數(shù)據(jù)。為了實(shí)現(xiàn)這一目標(biāo),SDS 的 API 全部做到了二進(jìn)制安全(binary-safe)。

2.2 raw 和 embstr 編碼的 SDS 區(qū)別

我們在前面講過,長度大于 39 字節(jié)的字符串,編碼類型為 raw,底層數(shù)據(jù)結(jié)構(gòu)是簡單動(dòng)態(tài)字符串(SDS)。這個(gè)很好理解,比如當(dāng)我們執(zhí)行 set story "Long, long, long ago there lived a king ..."(長度大于39)之后,Redis 就會(huì)創(chuàng)建一個(gè) raw 編碼的 String 對象。

數(shù)據(jù)結(jié)構(gòu)如下:

長度小于等于 39 個(gè)字節(jié)的字符串,編碼類型為 embstr,底層數(shù)據(jù)結(jié)構(gòu)則是 embstr 編碼 SDS。embstr 編碼是專門用來保存短字符串的,它和 raw 編碼最大的不同在于:raw 編碼會(huì)調(diào)用兩次內(nèi)存分配分別創(chuàng)建 redisObject 結(jié)構(gòu)和 sdshdr 結(jié)構(gòu);而 embstr 編碼則是只調(diào)用一次內(nèi)存分配,在一塊連續(xù)的空間上同時(shí)包含 redisObject 結(jié)構(gòu)和 sdshdr 結(jié)構(gòu)。

2.3 編碼轉(zhuǎn)換

int 編碼和 embstr 編碼的字符串對象在條件滿足的情況下會(huì)自動(dòng)轉(zhuǎn)換為 raw 編碼的字符串對象。

對于 int 編碼來說,當(dāng)我們修改這個(gè)字符串為不再是整數(shù)值的時(shí)候,此時(shí)字符串對象的編碼就會(huì)從 int 變?yōu)?raw。

對于 embstr 編碼來說,只要我們修改了字符串的值,此時(shí)字符串對象的編碼就會(huì)從 embstr 變?yōu)?raw。

embstr 編碼的字符串對象可以認(rèn)為是只讀的,因?yàn)?Redis 為其編寫任何修改程序。當(dāng)我們要修改 embstr 編碼字符串時(shí),都是先將轉(zhuǎn)換為 raw 編碼,然后再進(jìn)行修改。

3. 列表對象

列表對象的編碼可以是 linkedlist 或者 ziplist,對應(yīng)的底層數(shù)據(jù)結(jié)構(gòu)是鏈表和壓縮列表。列表對象相關(guān)命令可參考:Redis 命令-List。

默認(rèn)情況下,當(dāng)列表對象保存的所有字符串元素的長度都小于 64 字節(jié),且元素個(gè)數(shù)小于 512 個(gè)時(shí),列表對象采用的是 ziplist 編碼,否則使用 linkedlist 編碼。

可以通過配置文件修改該上限值。

3.1 鏈表

鏈表是一種非常常見的數(shù)據(jù)結(jié)構(gòu),提供了高效的節(jié)點(diǎn)重排能力以及順序性的節(jié)點(diǎn)訪問方式。在 Redis 中,每個(gè)鏈表節(jié)點(diǎn)使用 listNode 結(jié)構(gòu)表示:

typedef struct listNode { // 前置節(jié)點(diǎn) struct listNode *prev; // 后置節(jié)點(diǎn) struct listNode *next; // 節(jié)點(diǎn)值 void *value;} listNode

多個(gè) listNode 通過 prev 和 next 指針組成雙端鏈表,如下圖所示:

為了操作起來比較方便,Redis 使用了 list 結(jié)構(gòu)持有鏈表。

typedef struct list { // 表頭節(jié)點(diǎn) listNode *head; // 表尾節(jié)點(diǎn) listNode *tail; // 鏈表包含的節(jié)點(diǎn)數(shù)量 unsigned long len; // 節(jié)點(diǎn)復(fù)制函數(shù) void *(*dup)(void *ptr); // 節(jié)點(diǎn)釋放函數(shù) void (*free)(void *ptr); // 節(jié)點(diǎn)對比函數(shù) int (*match)(void *ptr, void *key);} list;

list 結(jié)構(gòu)為鏈表提供了表頭指針 head、表尾指針 tail,以及鏈表長度計(jì)數(shù)器 len,而 dup、free 和 match 成員則是實(shí)現(xiàn)多態(tài)鏈表所需類型的特定函數(shù)。

Redis 鏈表實(shí)現(xiàn)的特征總結(jié)如下:

雙端:鏈表節(jié)點(diǎn)帶有 prev 和 next 指針,獲取某個(gè)節(jié)點(diǎn)的前置節(jié)點(diǎn)和后置節(jié)點(diǎn)的復(fù)雜度都是O(n);

無環(huán):表頭節(jié)點(diǎn)的 prev 指針和表尾節(jié)點(diǎn)的 next 指針都指向 NULL,對鏈表的訪問以 NULL 為終點(diǎn);

帶表頭指針和表尾指針:通過 list 結(jié)構(gòu)的 head 指針和 tail 指針,程序獲取鏈表的表頭節(jié)點(diǎn)和表尾節(jié)點(diǎn)的復(fù)雜度為O(1);

帶鏈表長度計(jì)數(shù)器:程序使用 list 結(jié)構(gòu)的 len 屬性來對 list 持有的節(jié)點(diǎn)進(jìn)行計(jì)數(shù),程序獲取鏈表中節(jié)點(diǎn)數(shù)量的復(fù)雜度為O(1);

多態(tài):鏈表節(jié)點(diǎn)使用 void* 指針來保存節(jié)點(diǎn)值,可以保存各種不同類型的值。

3.2 壓縮列表

壓縮列表(ziplist)是列表鍵和哈希鍵的底層實(shí)現(xiàn)之一。壓縮列表主要目的是為了節(jié)約內(nèi)存,是由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序型數(shù)據(jù)結(jié)構(gòu)。一個(gè)壓縮列表可以包含任意多個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)可以保存一個(gè)字節(jié)數(shù)組或者一個(gè)整數(shù)值。

如上圖所示,壓縮列表記錄了各組成部分的類型、長度以及用途。

4. 哈希對象

哈希對象的編碼可以是 ziplist 或者 hashtable。

4.1 hash-ziplist

ziplist 底層使用的是壓縮列表實(shí)現(xiàn),上文已經(jīng)詳細(xì)介紹了壓縮列表的實(shí)現(xiàn)原理。每當(dāng)有新的鍵值對要加入哈希對象時(shí),先把保存了鍵的節(jié)點(diǎn)推入壓縮列表表尾,然后再將保存了值的節(jié)點(diǎn)推入壓縮列表表尾。比如,我們執(zhí)行如下三條 HSET 命令:

HSET profile name "tom"HSET profile age 25HSET profile career "Programmer"

如果此時(shí)使用 ziplist 編碼,那么該 Hash 對象在內(nèi)存中的結(jié)構(gòu)如下:

3.2 hash-hashtable

hashtable 編碼的哈希對象使用字典作為底層實(shí)現(xiàn)。字典是一種用于保存鍵值對的數(shù)據(jù)結(jié)構(gòu),Redis 的字典使用哈希表作為底層實(shí)現(xiàn),一個(gè)哈希表里面可以有多個(gè)哈希表節(jié)點(diǎn),每個(gè)哈希表節(jié)點(diǎn)保存的就是一個(gè)鍵值對。

3.3 哈希表

Redis 使用的哈希表由 dictht 結(jié)構(gòu)定義:

typedef struct dictht{ // 哈希表數(shù)組 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩碼,用于計(jì)算索引值 // 總是等于 size-1 unsigned long sizemask; // 該哈希表已有節(jié)點(diǎn)數(shù)量 unsigned long used;} dictht

table 屬性是一個(gè)數(shù)組,數(shù)組中的每個(gè)元素都是一個(gè)指向 dictEntry 結(jié)構(gòu)的指針,每個(gè) dictEntry 結(jié)構(gòu)保存著一個(gè)鍵值對。

size 屬性記錄了哈希表的大小,即 table 數(shù)組的大小。used 屬性記錄了哈希表目前已有節(jié)點(diǎn)數(shù)量。sizemask 總是等于 size-1,這個(gè)值主要用于數(shù)組索引。

比如下圖展示了一個(gè)大小為 4 的空哈希表。

哈希表節(jié)點(diǎn)

哈希表節(jié)點(diǎn)使用 dictEntry 結(jié)構(gòu)表示,每個(gè) dictEntry 結(jié)構(gòu)都保存著一個(gè)鍵值對:

typedef struct dictEntry { // 鍵 void *key; // 值 union { void *val; unit64_t u64; nit64_t s64; } v; // 指向下一個(gè)哈希表節(jié)點(diǎn),形成鏈表 struct dictEntry *next;} dictEntry;

key 屬性保存著鍵值對中的鍵,而 v 屬性則保存了鍵值對中的值。值可以是一個(gè)指針,一個(gè) uint64_t 整數(shù)或者是 int64_t 整數(shù)。next 屬性指向了另一個(gè) dictEntry 節(jié)點(diǎn),在數(shù)組桶位相同的情況下,將多個(gè) dictEntry 節(jié)點(diǎn)串聯(lián)成一個(gè)鏈表,以此來解決鍵沖突問題(鏈地址法)。

3.4 字典

Redis 字典由 dict 結(jié)構(gòu)表示:

typedefstructdict{ // 類型特定函數(shù) dictType *type; // 私有數(shù)據(jù) void *privdata; // 哈希表 dictht ht[2]; //rehash索引 // 當(dāng)rehash不在進(jìn)行時(shí),值為-1 int rehashidx;}

ht 是大小為 2,且每個(gè)元素都指向 dictht 哈希表。一般情況下,字典只會(huì)使用 ht[0] 哈希表,ht[1] 哈希表只會(huì)在對 ht[0] 哈希表進(jìn)行 rehash 時(shí)使用。rehashidx 記錄了 rehash 的進(jìn)度,如果目前沒有進(jìn)行 rehash,值為 -1。

rehash

為了使 hash 表的負(fù)載因子 (ht[0]).used/ht[0]).size) 維持在一個(gè)合理范圍,當(dāng)哈希表保存的元素過多或者過少時(shí),程序需要對 hash 表進(jìn)行相應(yīng)的擴(kuò)展和收縮。

rehash(重新散列)操作就是用來完成 hash 表的擴(kuò)展和收縮的。

rehash 的步驟如下:

1. 為 ht [1] 哈希表分配空間;

如果是擴(kuò)展操作,那么 ht[1] 的大小為第一個(gè)大于 ht[0].used*2的2n。比如ht[0].used=5,那么此時(shí) ht[1] 的大小就為 16(大于 10 的第一個(gè) 2n 的值是 16);

如果是收縮操作,那么 ht[1] 的大小為第一個(gè)大于 ht[0].used 的 2n。比如ht[0].used=5,那么此時(shí) ht[1] 的大小就為 8(大于 5 的第一個(gè) 2n 的值是 8)。

2. 將保存在 ht[0] 中的所有鍵值對 rehash 到 ht[1] 中;

3. 遷移完成之后,釋放掉 ht[0],并將現(xiàn)在的 ht[1] 設(shè)置為 ht[0],在 ht[1] 新創(chuàng)建一個(gè)空白哈希表,為下一次 rehash 做準(zhǔn)備。

哈希表的擴(kuò)展和收縮時(shí)機(jī)

當(dāng)服務(wù)器沒有執(zhí)行 BGSAVE 或者 BGREWRITEAOF 命令時(shí),負(fù)載因子大于等于 1 觸發(fā)哈希表的擴(kuò)展操作;

當(dāng)服務(wù)器在執(zhí)行 BGSAVE 或者 BGREWRITEAOF 命令,負(fù)載因子大于等于 5 觸發(fā)哈希表的擴(kuò)展操作;

當(dāng)哈希表負(fù)載因子小于 0.1,觸發(fā)哈希表的收縮操作。

漸進(jìn)式 rehash

前面講過,擴(kuò)展或者收縮需要將 ht[0] 里面的元素全部 rehash 到 ht[1] 中,如果 ht[0] 元素很多,顯然一次性 rehash 成本會(huì)很大,從影響到 Redis 性能。

為了解決上述問題,Redis 使用了漸進(jìn)式 rehash 技術(shù),具體來說就是分多次,漸進(jìn)式地將 ht[0] 里面的元素慢慢地 rehash 到 ht[1] 中。

下面是漸進(jìn)式 rehash 的詳細(xì)步驟:

為 ht[1] 分配空間;

在字典中維持一個(gè)索引計(jì)數(shù)器變量 rehashidx,并將它的值設(shè)置為 0,表示 rehash 正式開始;

在 rehash 進(jìn)行期間,每次對字典執(zhí)行添加、刪除、查找或者更新時(shí),除了會(huì)執(zhí)行相應(yīng)的操作之外,還會(huì)順帶將 ht[0] 在 rehashidx 索引位上的所有鍵值對 rehash 到 ht[1] 中,rehash 完成之后,rehashidx 值加 1;

隨著字典操作的不斷進(jìn)行,最終會(huì)在啊某個(gè)時(shí)刻遷移完成,此時(shí)將 rehashidx 值置為 -1,表示 rehash 結(jié)束。

漸進(jìn)式 rehash 一次遷移一個(gè)桶上所有的數(shù)據(jù)。設(shè)計(jì)上采用分而治之的思想,將原本集中式的操作分散到每個(gè)添加、刪除、查找和更新操作上,從而避免集中式 rehash 帶來的龐大計(jì)算。

因?yàn)樵跐u進(jìn)式 rehash 時(shí),字典會(huì)同時(shí)使用 ht[0] 和 ht[1] 兩張表,所以此時(shí)對字典的刪除、查找和更新操作都可能會(huì)在兩個(gè)哈希表進(jìn)行。比如,如果要查找某個(gè)鍵時(shí),先在 ht[0] 中查找,如果沒找到,則繼續(xù)到 ht[1] 中查找。

hash 對象中的 hashtable

HSET profile name "tom"HSET profile age 25HSET profile career "Programmer"

還是上述三條命令,保存數(shù)據(jù)到 Redis 的哈希對象中,如果采用 hashtable 編碼保存的話,那么該 Hash 對象在內(nèi)存中的結(jié)構(gòu)如下:

當(dāng)哈希對象保存的所有鍵值對的鍵和值的字符串長度都小于 64 個(gè)字節(jié),并且數(shù)量小于 512 個(gè)時(shí),使用 ziplist 編碼,否則使用 hashtable 編碼。

可以通過配置文件修改該上限值。

4. 集合對象

集合對象的編碼可以是 intset 或者 hashtable。當(dāng)集合對象保存的元素都是整數(shù),并且個(gè)數(shù)不超過 512 個(gè)時(shí),使用 intset 編碼,否則使用 hashtable 編碼。

4.1 set-intset

intset 編碼的集合對象底層使用整數(shù)集合實(shí)現(xiàn)。

整數(shù)集合(intset)是 Redis 用于保存整數(shù)值的集合抽象數(shù)據(jù)結(jié)構(gòu),它可以保存類型為 int16_t、int32_t 或者 int64_t 的整數(shù)值,并且保證集合中的數(shù)據(jù)不會(huì)重復(fù)。Redis 使用 intset 結(jié)構(gòu)表示一個(gè)整數(shù)集合。

typedef struct intset { // 編碼方式 uint32_t encoding; // 集合包含的元素?cái)?shù)量 uint32_t length; // 保存元素的數(shù)組 int8_t contents[];} intset;

contents 數(shù)組是整數(shù)集合的底層實(shí)現(xiàn):整數(shù)集合的每個(gè)元素都是 contents 數(shù)組的一個(gè)數(shù)組項(xiàng),各個(gè)項(xiàng)在數(shù)組中按值大小從小到大有序排列,并且數(shù)組中不包含重復(fù)項(xiàng)。

雖然 contents 屬性聲明為 int8_t 類型的數(shù)組,但實(shí)際上,contents 數(shù)組不保存任何 int8_t 類型的值,數(shù)組中真正保存的值類型取決于 encoding。

如果 encoding 屬性值為 INTSET_ENC_INT16,那么 contents 數(shù)組就是 int16_t 類型的數(shù)組,以此類推。

當(dāng)新插入元素的類型比整數(shù)集合現(xiàn)有類型元素的類型大時(shí),整數(shù)集合必須先升級,然后才能將新元素添加進(jìn)來。這個(gè)過程分以下三步進(jìn)行:

根據(jù)新元素類型,擴(kuò)展整數(shù)集合底層數(shù)組空間大小;

將底層數(shù)組現(xiàn)有所有元素都轉(zhuǎn)換為與新元素相同的類型,并且維持底層數(shù)組的有序性;

將新元素添加到底層數(shù)組里面。

還有一點(diǎn)需要注意的是,整數(shù)集合不支持降級。一旦對數(shù)組進(jìn)行了升級,編碼就會(huì)一直保持升級后的狀態(tài)。

舉個(gè)例子,當(dāng)執(zhí)行 SADD numbers 1 3 5 向集合對象插入數(shù)據(jù)時(shí),該集合對象在內(nèi)存的結(jié)構(gòu)如下:

4.2 set-hashtable

hashtable 編碼的集合對象使用字典作為底層實(shí)現(xiàn)。字典的每個(gè)鍵都是一個(gè)字符串對象,每個(gè)字符串對象對應(yīng)一個(gè)集合元素,字典的值都是 NULL。

當(dāng)我們執(zhí)行 SADD fruits "apple" "banana" "cherry" 向集合對象插入數(shù)據(jù)時(shí),該集合對象在內(nèi)存的結(jié)構(gòu)如下:

5. 有序集合對象

有序集合的編碼可以是 ziplist 或者 skiplist。當(dāng)有序集合保存的元素個(gè)數(shù)小于 128 個(gè),且所有元素成員長度都小于 64 字節(jié)時(shí),使用 ziplist 編碼,否則使用 skiplist 編碼。

5.1 zset-ziplist

ziplist 編碼的有序集合使用壓縮列表作為底層實(shí)現(xiàn)。每個(gè)集合元素使用兩個(gè)緊挨著一起的兩個(gè)壓縮列表節(jié)點(diǎn)表示,第一個(gè)節(jié)點(diǎn)保存元素的成員(member),第二個(gè)節(jié)點(diǎn)保存元素的分值(score)。

壓縮列表內(nèi)的集合元素按照分值從小到大排列。如果我們執(zhí)行 ZADD price 8.5 apple 5.0 banana 6.0 cherry 命令向有序集合插入元素,該有序集合在內(nèi)存中的結(jié)構(gòu)如下:

5.2 zset-skiplist

skiplist 編碼的有序集合對象使用 zset 結(jié)構(gòu)作為底層實(shí)現(xiàn),一個(gè) zset 結(jié)構(gòu)同時(shí)包含一個(gè)字典和一個(gè)跳躍表。

typedef struct zset { zskiplist *zs1; dict *dict;}

繼續(xù)介紹之前,我們先了解一下什么是跳躍表。

跳躍表

跳躍表(skiplist)是一種有序的數(shù)據(jù)結(jié)構(gòu),它通過在每個(gè)節(jié)點(diǎn)中維持多個(gè)指向其他節(jié)點(diǎn)的指針,從而達(dá)到快速訪問節(jié)點(diǎn)的目的。

Redis 的跳躍表由 zskiplistNode 和 zskiplist 兩個(gè)結(jié)構(gòu)定義。zskiplistNode 結(jié)構(gòu)表示跳躍表節(jié)點(diǎn),zskiplist 保存跳躍表節(jié)點(diǎn)相關(guān)信息,比如節(jié)點(diǎn)的數(shù)量,以及指向表頭和表尾節(jié)點(diǎn)的指針等。

跳躍表節(jié)點(diǎn) zskiplistNode

跳躍表節(jié)點(diǎn) zskiplistNode 結(jié)構(gòu)定義如下:

typedef struct zskiplistNode { // 后退指針 struct zskiplistNode *backward; // 分值 double score; // 成員對象 robj *obj; // 層 struct zskiplistLevel { // 前進(jìn)指針 struct zskiplistNode *forward; // 跨度 unsigned int span; } level[];} zskiplistNode;

下圖是一個(gè)層高為 5,包含 4 個(gè)跳躍表節(jié)點(diǎn)(1 個(gè)表頭節(jié)點(diǎn)和 3 個(gè)數(shù)據(jù)節(jié)點(diǎn))組成的跳躍表:

有序集合對象的 skiplist 實(shí)現(xiàn)

前面講過,skiplist 編碼的有序集合對象使用 zset 結(jié)構(gòu)作為底層實(shí)現(xiàn)。一個(gè) zset 結(jié)構(gòu)同時(shí)包含一個(gè)字典和一個(gè)跳躍表。

typedef struct zset { zskiplist *zs1; dict *dict;}

zset 結(jié)構(gòu)中的 zs1 跳躍表按分值從小到大保存了所有集合元素,每個(gè)跳躍表節(jié)點(diǎn)都保存了一個(gè)集合元素。

通過跳躍表,可以對有序集合進(jìn)行基于 score 的快速范圍查找。zset 結(jié)構(gòu)中的 dict 字典為有序集合創(chuàng)建了從成員到分值的映射,字典的鍵保存了成員,字典的值保存了分值。通過字典,可以用O(1)復(fù)雜度查找給定成員的分值。

假如還是執(zhí)行 ZADD price 8.5 apple 5.0 banana 6.0 cherry 命令向 zset 保存數(shù)據(jù),如果采用 skiplist 編碼方式的話,該有序集合在內(nèi)存中的結(jié)構(gòu)如下:

6. 總結(jié)

總的來說,Redis 底層數(shù)據(jù)結(jié)構(gòu)主要包括簡單動(dòng)態(tài)字符串(SDS)、鏈表、字典、跳躍表、整數(shù)集合和壓縮列表六種類型。并且基于這些基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)了字符串對象、列表對象、哈希對象、集合對象以及有序集合對象五種常見的對象類型。每一種對象類型都至少采用了 2 種數(shù)據(jù)編碼,不同的編碼使用的底層數(shù)據(jù)結(jié)構(gòu)也不同。

責(zé)任編輯:xj

原文標(biāo)題:一文讀懂 Redis 常見對象類型的底層數(shù)據(jù)結(jié)構(gòu)

文章出處:【微信公眾號:數(shù)據(jù)分析與開發(fā)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • 數(shù)據(jù)結(jié)構(gòu)

    關(guān)注

    3

    文章

    573

    瀏覽量

    41603
  • Redis
    +關(guān)注

    關(guān)注

    0

    文章

    392

    瀏覽量

    12191

原文標(biāo)題:一文讀懂 Redis 常見對象類型的底層數(shù)據(jù)結(jié)構(gòu)

文章出處:【微信號:DBDevs,微信公眾號:數(shù)據(jù)分析與開發(fā)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關(guān)推薦
    熱點(diǎn)推薦

    基于凌羽派的OpenHarmony北向應(yīng)用開發(fā):ArkTS語法-數(shù)據(jù)類型和變量聲明

    和訪問都是直接的,比較時(shí)直接比較其值。 引用類型包括對象、數(shù)組和函數(shù)等復(fù)雜數(shù)據(jù)結(jié)構(gòu)。這些類型通過引用訪問數(shù)據(jù)
    發(fā)表于 02-26 14:24

    種類型內(nèi)存的使用

    的,因?yàn)?b class='flag-5'>底層數(shù)據(jù)會(huì)被默默刪除。自動(dòng)存儲(chǔ)通常被稱為“?!?。 分配的存儲(chǔ):運(yùn)行malloc() 會(huì)返回的內(nèi)存類型,這種內(nèi)存會(huì)一直保留,直到被 free() 函數(shù)釋放,所以可以被傳遞到任何地方,包括返回
    發(fā)表于 12-12 06:43

    typedef結(jié)構(gòu)體使用

    雖然結(jié)構(gòu)體的出現(xiàn)能夠讓我們有一個(gè)更科學(xué)的數(shù)據(jù)結(jié)構(gòu)來管理數(shù)據(jù),但是每次使用結(jié)構(gòu)體都需要struct...,未免顯得有些冗長和麻煩。有了typedef的助攻,我們就可以很輕松地給
    發(fā)表于 12-08 07:04

    HDMI接口類型介紹

    我們都知道USB接口有很多類型,然而熟悉的HDMI接口,它也有很多不一樣的接口,本文將圍繞HDMI的不同接口類型進(jìn)行解析。
    的頭像 發(fā)表于 10-28 16:11 ?5777次閱讀
    <b class='flag-5'>五</b><b class='flag-5'>種</b>HDMI接口<b class='flag-5'>類型</b>介紹

    【HZ-T536開發(fā)板免費(fèi)體驗(yàn)】6、使用protoc-gen-gorm生成標(biāo)準(zhǔn)化的數(shù)據(jù)結(jié)構(gòu)

    在設(shè)計(jì)espnow協(xié)議的時(shí)候,考慮到我需要在esp32,Linux設(shè)備,web上使用相同的數(shù)據(jù)結(jié)構(gòu),那就需要考慮一下,是否使用一個(gè)通用的跨平臺序列化數(shù)據(jù)結(jié)構(gòu)。這時(shí)候我想起了protobuf,這個(gè)就是
    發(fā)表于 08-26 00:32

    Redis集群部署配置詳解

    Redis集群是一分布式Redis解決方案,通過數(shù)據(jù)分片和主從復(fù)制實(shí)現(xiàn)高可用性和橫向擴(kuò)展。集群將整個(gè)數(shù)據(jù)集分割成16384個(gè)哈希槽(has
    的頭像 發(fā)表于 07-17 11:04 ?1007次閱讀

    Redis集群部署與性能優(yōu)化實(shí)戰(zhàn)

    Redis作為高性能的內(nèi)存數(shù)據(jù)庫,在現(xiàn)代互聯(lián)網(wǎng)架構(gòu)中扮演著關(guān)鍵角色。作為運(yùn)維工程師,掌握Redis的部署、配置和優(yōu)化技能至關(guān)重要。本文將從實(shí)戰(zhàn)角度出發(fā),詳細(xì)介紹Redis集群的搭建、性
    的頭像 發(fā)表于 07-08 17:56 ?867次閱讀

    常見的溫濕度傳感器類型?

    溫濕度傳感器是一用于測量環(huán)境溫度和濕度的設(shè)備,廣泛應(yīng)用于工業(yè)、農(nóng)業(yè)、氣象等領(lǐng)域。以下是幾種常見的溫濕度傳感器類型及其優(yōu)缺點(diǎn): 電容式濕度傳感器 電容式濕度傳感器基于介電常數(shù)與相對濕度之間的關(guān)系來
    發(fā)表于 06-24 09:24

    【幸狐Omni3576邊緣計(jì)算套件試用體驗(yàn)】Redis最新8.0.2版本源碼安裝及性能測試

    的結(jié)果進(jìn)行對比。 一、Redis是什么 維基百科的介紹是: Redis是一個(gè)使用ANSI C編寫的開源、支持網(wǎng)絡(luò)、基于內(nèi)存、分布式、可選持久性的鍵值對存儲(chǔ)數(shù)據(jù)庫。 Redis官網(wǎng)的
    發(fā)表于 06-03 01:28

    程序設(shè)計(jì)與數(shù)據(jù)結(jié)構(gòu)

    《程序設(shè)計(jì)與數(shù)據(jù)結(jié)構(gòu)》重點(diǎn)闡述了三大方向內(nèi)容: 1. C語言學(xué)習(xí)中的痛點(diǎn):針對當(dāng)前工程師在C語言學(xué)習(xí)中的痛點(diǎn),如指針函數(shù)與函數(shù)指針,如何靈活應(yīng)用結(jié)構(gòu)體等。從變量的三要素(變量的類型,變量的值和變量
    發(fā)表于 05-13 16:45

    層數(shù)層疊結(jié)構(gòu)PCB的布線策略

    層數(shù) PCB 的布線策略豐富多樣,具體取決于 PCB 的功能。這類電路板可能涉及多種不同類型的信號,從低速數(shù)字接口到具有不同信號完整性要求的多個(gè)高速數(shù)字接口。從布線規(guī)劃和為各接口分配信號層的角度來看,這無疑是一項(xiàng)極具挑戰(zhàn)性的任務(wù)。
    的頭像 發(fā)表于 05-07 14:50 ?1641次閱讀
    高<b class='flag-5'>層數(shù)</b>層疊<b class='flag-5'>結(jié)構(gòu)</b>PCB的布線策略

    請問K230D怎么將攝像頭采集的視頻數(shù)據(jù)通過串口輸出?

    我連了個(gè)WiFi模塊,想要將攝像頭采集的視頻數(shù)據(jù)通過串口發(fā)送出去。之前都是用的STM32,不太會(huì)MicroPython,搞不懂對象數(shù)據(jù)結(jié)構(gòu),求教。
    發(fā)表于 04-28 06:16

    在EMC中,MOSFET 柵極驅(qū)動(dòng)電路常見類型

    在EMC中,MOSFET 柵極驅(qū)動(dòng)電路常見類型
    的頭像 發(fā)表于 04-14 16:48 ?1152次閱讀
    在EMC中,MOSFET 柵極驅(qū)動(dòng)電路<b class='flag-5'>常見</b><b class='flag-5'>類型</b>

    C語言中結(jié)構(gòu)體與聯(lián)合體的深度解析:內(nèi)存布局與應(yīng)用場景

    在于對內(nèi)存的極致操控。結(jié)構(gòu)體構(gòu)建數(shù)據(jù)實(shí)體,聯(lián)合體實(shí)現(xiàn)內(nèi)存復(fù)用,二者的組合使用能創(chuàng)造出強(qiáng)大的數(shù)據(jù)結(jié)構(gòu)。掌握它們的底層原理,配合內(nèi)存分析工具(如Valgrind、GDB),將助你在嵌入式開
    發(fā)表于 04-08 09:18

    redis集群方案詳解

    Redis中提供的集群方案總共有三(一般一個(gè)redis節(jié)點(diǎn)不超過10G內(nèi)存)。
    的頭像 發(fā)表于 03-31 10:46 ?1539次閱讀
    <b class='flag-5'>redis</b>三<b class='flag-5'>種</b>集群方案詳解