Redis數(shù)據(jù)編碼詳解
struct redisObject {
unsigned type:4; // [0-3 bit] 對象類型 (如 String)
unsigned encoding:4; // [4-7 bit] 編碼方式 (如 int/embstr/raw)
unsigned lru:24; // [8-31 bit] 緩存淘汰數(shù)據(jù)
int refcount; // [32-63 bit] 引用計數(shù) (4字節(jié))
void *ptr; // [64-127 bit] 關(guān)鍵指針 (8字節(jié))
};String
在 Redis 的底層實現(xiàn)中,String(字符串) 類型并不只有一種形態(tài)。為了平衡“內(nèi)存占用”與“處理性能”,Redis 會根據(jù)字符串的內(nèi)容和長度,在 int、embstr 和 raw 三種編碼方式之間自動切換。
這三種編碼都封裝在 redisObject 這個“外殼”下,通過 encoding 字段進行區(qū)分。
struct sdshdr8 {
uint8_t len; /* 已使用長度 */
uint8_t alloc; /* 總分配空間(不含頭和 \0) */
unsigned char flags; /* 類型標(biāo)志(如 sdshdr8, sdshdr16 等) */
char buf[]; /* 實際字節(jié)數(shù)組 */
};1.int編碼:直接存儲整數(shù)
當(dāng)一個字符串對象保存的是整數(shù)值,且這個整數(shù)可以用 long 類型(8 字節(jié)有符號整數(shù))表示時,Redis 就會使用 int 編碼。
- 物理特征:它不會分配額外的 SDS 空間,而是直接將整數(shù)值存儲在
redisObject結(jié)構(gòu)體的ptr指針字段中(通過強制類型轉(zhuǎn)換)。 - 共享對象優(yōu)化:Redis 啟動時會預(yù)先創(chuàng)建 0 ∼ 9999 0 \sim 9999 0∼9999 這 10,000 個整數(shù)對象。如果你存的值在這個范圍內(nèi),所有的 Key 都會指向同一個物理內(nèi)存地址,引用計數(shù)加 1,內(nèi)存開銷幾乎為零。
- 適用場景:計數(shù)器、ID 存儲等數(shù)值場景。
2.embstr編碼:嵌入式短字符串
當(dāng)字符串的長度 小于等于 44 字節(jié) 時,Redis 使用 embstr 編碼。這是為了極致壓榨小對象的性能。
- 物理特征:
redisObject和sdshdr(SDS 頭部及數(shù)據(jù))在內(nèi)存中是連續(xù)的一整塊。它是通過一次malloc申請出來的。 - 核心邏輯:
- 只讀性:它是只讀的,任何修改操作(如
APPEND)都會迫使它先升級為raw。 - 高性能:由于內(nèi)存連續(xù),CPU 緩存命中率極高,且分配/釋放內(nèi)存只需要一次系統(tǒng)調(diào)用。
- 只讀性:它是只讀的,任何修改操作(如
- 計算門檻:44 字節(jié)的限制是為了讓整個對象(16B
redisObject+ 3Bsdshdr8+ 1B\0+ 44B Data)剛好適配內(nèi)存分配器的 64 字節(jié) 內(nèi)存槽位。
3.raw編碼:常規(guī)長字符串
當(dāng)字符串的長度 大于 44 字節(jié),或者對 embstr 進行了修改操作時,Redis 會使用 raw 編碼。
- 物理特征:
redisObject和sdshdr分布在兩塊不連續(xù)的內(nèi)存空間中。ptr指針指向獨立的 SDS 區(qū)域。 - 核心邏輯:
- 可擴展性:適合存儲長文本、二進制數(shù)據(jù)或頻繁修改的字符串。
- 分配代價:創(chuàng)建或銷毀對象需要兩次
malloc或free。
- 適用場景:JSON 數(shù)據(jù)、序列化后的對象、較大的文本內(nèi)容。
List
Redis3.2之前:ZipList/LinkedList
在 Redis 3.2 之前,List 的實現(xiàn)非常簡單粗暴:當(dāng)數(shù)據(jù)量小時使用 ZipList(壓縮列表),通過連續(xù)內(nèi)存壓榨空間;當(dāng)數(shù)據(jù)量大或字符串長時,直接轉(zhuǎn)換為 LinkedList(雙向鏈表),通過指針實現(xiàn)靈活增刪,但代價是每個節(jié)點都要背負(fù)兩個 8 字節(jié)指針的沉重負(fù)擔(dān),且內(nèi)存碎片極多。
Redis3.2之后:QuickList
RedisObject中的*ptr指向quicklist對象
typedef struct quicklist {
quicklistNode *head; /* 指向頭節(jié)點 */
quicklistNode *tail; /* 指向尾節(jié)點 */
unsigned long count; /* 所有元素總數(shù) */
unsigned long len; /* 節(jié)點(車廂)總數(shù) */
int fill : 16; /* 節(jié)點填充因子 */
unsigned int compress : 16; /* 壓縮深度 */
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; /* 前驅(qū)指針 */
struct quicklistNode *next; /* 后繼指針 */
unsigned char *zl; /* 指向物理內(nèi)存中的連續(xù)塊 (ZipList/Listpack) */
unsigned int sz; /* 連續(xù)塊占用的總字節(jié)數(shù) */
unsigned int count : 16; /* 連續(xù)塊包含的元素個數(shù) */
// ... 其他標(biāo)志位
} quicklistNode;Set
Redis 的 Set(集合) 編碼設(shè)計同樣遵循“從小到大”的進化邏輯。它在物理實現(xiàn)上主要在 IntSet(整數(shù)集合)、Listpack(緊湊列表,Redis 7.2+) 和 Hashtable(哈希表) 之間切換。
它的核心哲學(xué)是:如果全是小整數(shù),我用數(shù)組排好序;如果有字符串,我用哈希表鎖死。
1. 物理結(jié)構(gòu):intset(整數(shù)集合)
當(dāng)集合滿足以下 兩個條件 時,Redis 優(yōu)先使用 intset:
- 集合內(nèi)所有成員均為 整數(shù)。
- 成員數(shù)量小于配置參數(shù)
set-max-intset-entries(默認(rèn) 512 個)。
內(nèi)存布局與查找邏輯
intset 是一塊絕對連續(xù)的內(nèi)存空間。
- 物理存儲:內(nèi)部是一個有序數(shù)組,支持
int16_t、int32_t或int64_t編碼。 - 有序性:元素在數(shù)組內(nèi)按從小到大嚴(yán)格排序。
- 查找算法:使用 二分查找(Binary Search),時間復(fù)雜度為 O ( log ? N ) O(\log N) O(logN)。
- 升級邏輯:當(dāng)新插入的整數(shù)超出當(dāng)前位寬(如
int16存入int32)時,會觸發(fā)整塊內(nèi)存的重新分配和數(shù)據(jù)遷移。注意,為了保持效率,該過程不可逆(不支持降級)。
2. 物理結(jié)構(gòu):listpack(緊湊列表)
這是 Redis 7.2 引入的新物理層。在舊版本中,集合只要出現(xiàn)一個字符串就會立刻膨脹為 dict,而 listpack 充當(dāng)了中間的緩沖帶。
- 觸發(fā)場景:集合中包含字符串,但成員數(shù)量和單個字符串長度未達到
set-max-listpack-entries和set-max-listpack-value閾值。 - 物理特征:連續(xù)字節(jié)流存儲。
- 性能權(quán)衡:雖然查找復(fù)雜度退化為 O ( N ) O(N) O(N)(順序遍歷),但由于數(shù)據(jù)規(guī)模極小,其內(nèi)存利用率遠高于
dict,且在小數(shù)據(jù)量下,連續(xù)內(nèi)存對 CPU 緩存的友好性抵消了 O ( N ) O(N) O(N) 的算法劣勢。
3. 物理結(jié)構(gòu):dict(字典 / 邏輯名稱 HashTable)
當(dāng)集合規(guī)模超過閾值,或包含長字符串時,Redis 會使用 dict 作為終極物理載體。
物理映射與內(nèi)存布局
此時 redisObject->ptr 指向一個真實的 dict 結(jié)構(gòu)體實例。
- Key (鍵):存儲集合的成員,指向一個 SDS 字符串對象。
- Value (值):物理上統(tǒng)一設(shè)置為
NULL指針。 - 唯一性保證:直接利用
dict自身的哈希碰撞處理和 Key 唯一性邏輯實現(xiàn)集合去重。 - 性能特征:查找復(fù)雜度為 O ( 1 ) O(1) O(1)。支持漸進式 Rehash,在數(shù)據(jù)量極大時仍能保持穩(wěn)定的響應(yīng)速度。
4. 宏觀物理映射:RedisObject 的指向
對于 Set 來說,redisObject 的包裝方式非常直觀:
| 字段 | IntSet 編碼 | Hashtable 編碼 |
|---|---|---|
| type | OBJ_SET | OBJ_SET |
| encoding | OBJ_ENCODING_INTSET | OBJ_ENCODING_HT |
| ptr 指向 | 一整塊連續(xù)的 intset 結(jié)構(gòu) | 一個復(fù)雜的 dict 字典結(jié)構(gòu) |
ZSet
Redis 的 ZSet(有序集合) 在底層編碼上設(shè)計得最為復(fù)雜,因為它必須同時滿足 O ( 1 ) O(1) O(1) 成員查分 和 O ( log ? N ) O(\log N) O(logN) 按分?jǐn)?shù)排序/范圍檢索 這兩個核心需求。
其物理實現(xiàn)主要分為兩個階段:listpack 和 dict + zskiplist。
1. 緊湊階段:listpack(緊湊列表)
當(dāng) ZSet 滿足以下兩個條件時,Redis 使用 listpack 編碼(OBJ_ENCODING_LISTPACK):
- 成員數(shù)量小于
zset-max-listpack-entries(默認(rèn) 128)。 - 所有成員字符串長度小于
zset-max-listpack-value(默認(rèn) 64 字節(jié))。
物理存儲邏輯
在 listpack 內(nèi)部,成員(Member)和分值(Score)被存儲為兩個相鄰的 Entry:
- 布局:
[Member1, Score1, Member2, Score2, ...] - 有序性:內(nèi)部元素按分值(Score)從小到大嚴(yán)格排序。
- 性能特征:由于是連續(xù)內(nèi)存,插入和查找涉及 O ( N ) O(N) O(N) 的順序遍歷及內(nèi)存搬遷。但在小數(shù)據(jù)量下,這種結(jié)構(gòu)的 CPU 緩存命中率極高,且省去了復(fù)雜的指針開銷。
2. 進化階段:zset結(jié)構(gòu)體 (跳表 + 字典)
當(dāng)數(shù)據(jù)量突破閾值后,redisObject->ptr 會指向一個專門的 zset 結(jié)構(gòu)體。這是一個雙重物理結(jié)構(gòu)的組合:
typedef struct zset {
dict *dict; /* 成員 -> 分值的哈希表 */
zskiplist *zsl; /* 按分?jǐn)?shù)排序的跳躍表 */
} zset;A. 物理組件一:dict(字典)
- 作用:實現(xiàn) O ( 1 ) O(1) O(1) 復(fù)雜度的
ZSCORE操作。 - 邏輯:Key 是成員(SDS),Value 是分值(double)。
- 必要性:如果沒有
dict,查找一個成員的分?jǐn)?shù)需要遍歷跳表,復(fù)雜度為 O ( log ? N ) O(\log N) O(logN)。
B. 物理組件二:zskiplist(跳躍表)
- 作用:實現(xiàn)高效的范圍查詢(
ZRANGE)和排名計算(ZRANK)。 - 邏輯:節(jié)點按分?jǐn)?shù)排序。每個節(jié)點包含多個層級的指針,支持快速跳躍尋址。
- 性能:平均查找復(fù)雜度為 O ( log ? N ) O(\log N) O(logN)。
3. 內(nèi)存優(yōu)化:SDS 的“引用共享”
你可能會擔(dān)心:同一個成員既存在 dict 里,又存在 zskiplist 里,豈不是浪費了一倍內(nèi)存?
物理真相:dict 的 Key 和 zskiplistNode 的 ele 指向的是同一個物理內(nèi)存地址(同一個 SDS 對象)。
- Redis 只是在兩個數(shù)據(jù)結(jié)構(gòu)中各存了一個 指針。
- 這種設(shè)計通過增加少量指針開銷(每個節(jié)點約幾十字節(jié)),換取了兩個維度的極致查詢速度。
4. 物理特性對比表
| 物理結(jié)構(gòu) | 邏輯編碼 (Encoding) | 核心優(yōu)勢 | 算法復(fù)雜度 | 內(nèi)存特征 |
|---|---|---|---|---|
| listpack | LISTPACK | 極致節(jié)省內(nèi)存 | O ( N ) O(N) O(N) (查找/插入) | 連續(xù)內(nèi)存,無碎片 |
| zset (復(fù)合) | SKIPLIST | 全能性能 | 查分 O ( 1 ) O(1) O(1),范圍 O ( log ? N ) O(\log N) O(logN) | 雙重索引,指針較多 |
5. 狀態(tài)轉(zhuǎn)換邏輯
ZSet 的轉(zhuǎn)換通常是單向不可逆的:
- 一旦數(shù)據(jù)量超過閾值,
listpack會被拆解,重新裝載進一個新的dict和zskiplist中。 - 原因:從復(fù)雜的雙重結(jié)構(gòu)回退到連續(xù)內(nèi)存塊涉及大規(guī)模的內(nèi)存重分配和 CPU 計算,收益不抵成本。
Hash
Redis 的 Hash(哈希) 結(jié)構(gòu)在底層編碼的設(shè)計上,邏輯與 ZSet 非常相似:在數(shù)據(jù)量小時采用緊湊的連續(xù)內(nèi)存,在數(shù)據(jù)量大時進化為散列表。
目前的物理實現(xiàn)主要分為 listpack 和 dict 兩種。
1. 緊湊編碼:listpack(緊湊列表)
當(dāng) Hash 結(jié)構(gòu)滿足以下兩個條件時,Redis 使用 listpack 存儲(編碼名稱為 OBJ_ENCODING_LISTPACK):
- 哈希中字段(Field)的數(shù)量小于
hash-max-listpack-entries(默認(rèn) 512 個)。 - 所有字段名和值的長度都小于
hash-max-listpack-value(默認(rèn) 64 字節(jié))。
物理存儲邏輯
在 listpack 的字節(jié)流中,F(xiàn)ield 和 Value 是作為兩個相鄰的 Entry 存儲的:
- 布局:
[Field1, Value1, Field2, Value2, ...] - 查找方式:完全依靠順序遍歷。由于內(nèi)存是絕對連續(xù)的,CPU 在讀取時可以利用預(yù)取機制(Prefetching),在小規(guī)模數(shù)據(jù)下速度極快。
- 內(nèi)存優(yōu)勢:沒有指針開銷,沒有內(nèi)存對齊的空隙,空間利用率達到極致。
2. 散列編碼:dict(字典)
一旦數(shù)據(jù)量突破閾值,或者某個 Value 太長,Redis 就會將物理結(jié)構(gòu)轉(zhuǎn)換為 dict(編碼名稱為 OBJ_ENCODING_HT)。
物理實現(xiàn)邏輯
此時 redisObject->ptr 指向一個真實的 dict 結(jié)構(gòu)體:
- Key (鍵):存儲的是 Hash 的字段名(Field),物理上是一個 SDS 對象。
- Value (值):存儲的是 Hash 的字段值(Value),物理上同樣是一個 SDS 對象。
- 沖突處理:使用拉鏈法(鏈地址法)解決哈希沖突。
- 性能特征:查找、插入和刪除的復(fù)雜度均為 O ( 1 ) O(1) O(1)。
到此這篇關(guān)于Redis數(shù)據(jù)編碼詳解的文章就介紹到這了,更多相關(guān)redis數(shù)據(jù)編碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
CentOS7.5使用mysql_multi方式安裝MySQL5.7.28多實例(詳解)
這篇文章主要介紹了CentOS7.5使用mysql_multi方式安裝MySQL5.7.28多實例,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2020-01-01
Redis數(shù)據(jù)結(jié)構(gòu)之listpack和quicklist使用學(xué)習(xí)
這篇文章主要為大家介紹了Redis數(shù)據(jù)結(jié)構(gòu)之listpack和quicklist的使用學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07

