概述
我們平時用 Redis都是處於使用者層面,我們可能會不加思索地操作一個 key-value 對來方便地存取資料,感覺方便之至。但你知道這些資料在背後是如何儲存以及編碼的嗎? 瞭解清楚了這個問題,將對我們更加高效地使用 Redis具有指導意義。本文開始我們將結合 Redis原始碼來逐個探討Redis五大資料型別的內部編碼機制。
- 實驗環境:Redis 4.0.10
注: 本文首發於 My 公眾號 CodeSheep ,可 長按 或 掃描 下面的 小心心 來訂閱 ↓ ↓ ↓
Redis資料型別內部編碼概況
對於 Redis的常用 5 種資料型別(String、Hash、List、Set、sorted set),每種資料型別都提供了 最少兩種 內部的編碼格式,而且每個資料型別內部編碼方式的選擇 對使用者是完全透明的,Redis會根據資料量自適應地選擇較優化的內部編碼格式。
如果想檢視某個鍵的內部編碼格式,可以使用 OBJECT ENCODING keyname
指令來進行,比如:
127.0.0.1:6379>
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379>
127.0.0.1:6379> object encoding foo // 檢視某個Redis鍵值的編碼
"embstr"
127.0.0.1:6379>
127.0.0.1:6379>
複製程式碼
Redis
的每個鍵值內部都是使用一個名字叫做 redisObject
這個 C語言結構體儲存的,其程式碼如下:
解釋如下:
type
:表示鍵值的資料型別,包括 String、List、Set、ZSet、Hashencoding
:表示鍵值的內部編碼方式,從 Redis原始碼看目前取值有如下幾種:
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
複製程式碼
refcount
:表示該鍵值被引用的數量,即一個鍵值可被多個鍵引用
本文我們就從 Redis最基本的 String型別的內部編碼開始探討!
String型別的內部編碼情況
字串是 Redis最基本的資料型別,Redis 中字串物件的編碼可以是 int
,raw
或者 embstr
中的某一種,分別介紹如下:
- int 編碼:儲存long 型的64位有符號整數
- embstr 編碼:儲存長度小於44位元組的字串
- raw 編碼:儲存長度大於44位元組的字串
我們不妨來做個實驗實際看一下:
實際情況就是 Redis 內部會根據使用者給的不同鍵值而使用不同的編碼格式,而這一切對使用者完全透明!
Redis 是使用 SDS(“簡單動態字串”)這個結構體來儲存字串,程式碼裡定義了 5種 SDS結構體:
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
複製程式碼
可以看出,除了結構體欄位資料型別的不同,其欄位含義相差無幾,其中:
len
:字串的長度(實際使用的長度)alloc
:分配記憶體的大小flags
:標誌位,低三位表示型別,其餘五位未使用buf
:字元陣列
瞭解了這些基本的資料結構以後,我們就來看看上面例子中:
- set foo 123
- set foo abc
- set foo abcdefghijklmnopqrstuvwxyzabcdeffasdffsdaadsx
這三種情形下 Redis 內部到底是怎麼存資料的!
INT 編碼格式
命令示例: set foo 123
當字串鍵值的內容可以用一個 64位有符號整形 來表示時,Redis會將鍵值轉化為 long型來進行儲存,此時即對應 OBJ_ENCODING_INT
編碼型別。
OBJ_ENCODING_INT
編碼型別內部的記憶體結構可以形象地表示如下:
而且 Redis 啟動時會預先建立 10000 個分別儲存 0~9999 的 redisObject 變數作為共享物件,這就意味著如果 set字串的鍵值在 0~10000 之間的話,則可以 直接指向共享物件 而不需要再建立新物件,此時鍵值不佔空間!
因此,當執行如下指令時:
set key1 100
set key2 100
複製程式碼
其實 key1 和 key2 這兩個鍵值都直接引用了一個 Redis 預先已建立好的共享 redisObject 物件,就像下面這樣:
原始碼之前,了無祕密,我們再對照下面的原始碼,來理解一下上述過程
EMBSTR編碼格式
命令示例: set foo abc
Redis 在儲存長度小於 44 位元組的字串時會採用 OBJ_ENCODING_EMBSTR
編碼方式,口說無憑,我們來瞅瞅原始碼:
從上述程式碼中很容易看出,對於長度小於 44的字串,Redis 對鍵值採用OBJ_ENCODING_EMBSTR
方式,EMBSTR 顧名思義即:embedded string,表示嵌入式的String。從記憶體結構上來講 即字串 sds結構體與其對應的 redisObject 物件分配在 同一塊連續的記憶體空間,這就彷彿字串 sds 嵌入在 redisObject 物件之中一樣,這一切從下面的程式碼即可清楚地看到:
因此,對於指令 set foo abc
所設定的鍵值,其記憶體結構示意圖如下:
RAW 編碼格式
指令示例: set foo abcdefghijklmnopqrstuvwxyzabcdeffasdffsdaadsx
正如指令示例,當字串的鍵值為長度大於 44 的 超長字串 時,Redis 則會將鍵值的內部編碼方式改為 OBJ_ENCODING_RAW
格式,這與上面的 OBJ_ENCODING_EMBSTR
編碼方式的不同之處在於 此時動態字串 sds 的記憶體與其依賴的 redisObject 的 記憶體不再連續 了,以 set foo abcdefghijklmnopqrstuvwxyzabcdeffasdffsdaadsx
為例,其鍵值的記憶體結構如下所示:
到此就講完了最基本的String資料型別的內部編碼情況,怎麼樣,還是挺好理解的吧!
後續我們將繼續剖析 Redis 中 Hash 資料型別的內部編碼格式。
後 記
由於能力有限,若有錯誤或者不當之處,還請大家批評指正,一起學習交流!
作者更多的SpringBt實踐文章在此:
- 一隻菜雞的半年技術部落格之路
- Spring Boot日誌框架實踐
- Spring Boot Admin2.0開箱體驗
- Spring Boot應用監控實戰
- SpringBoot應用部署於外接Tomcat容器
- ElasticSearch搜尋引擎在SpringBt中的實踐
- 初探Kotlin+SpringBoot聯合程式設計
- SpringBoot優雅編碼之:Lombok加持
如果有興趣,也可以抽點時間看看作者一些關於容器化、微服務化方面的文章:
- 利用K8S技術棧打造個人私有云 連載文章
- 從一份配置清單詳解Nginx伺服器配置
- Docker容器視覺化監控中心搭建
- 利用ELK搭建Docker容器化應用日誌中心
- RPC框架實踐之:Apache Thrift
- RPC框架實踐之:Google gRPC
- 微服務呼叫鏈追蹤中心搭建
- Docker容器跨主機通訊
- Docker Swarm叢集初探
- 高效編寫Dockerfile的幾條準則
可 長按 或 掃描 下面的 小心心 來訂閱 CodeSheep,獲取更多 務實、能看懂、可復現的 原創文 ↓↓↓