前文 Redis 設計與實現 2:Redis 對象 說到,五大數據類型都會封裝成 RedisObject。
typedef struct redisObject {
unsigned type:4; // 類型
unsigned encoding:4; // 編碼
// ...
void *ptr; // 指向具體底層數據的指針
} robj;
不同數據類型的主要區別就是 type 和 encoding 屬性的差異,同一種數據類型,有不同的編碼。
一、編碼類型
字符串的編碼有raw、embstr、int三種。
raw用於長字符串。embstr用於短字符串。int用於整數類型。
定義在 server.h 中,這里只列出 string 類型的編碼
#define OBJ_ENCODING_RAW 0
#define OBJ_ENCODING_INT 1
#define OBJ_ENCODING_EMBSTR 8
編碼 1:raw
raw 編碼主要用來保存長度超過 44 的字符串。其真實數據,由 sdshdr 結構來表示存儲,外層還是由 redisObject 包裝。
sdshdr 的結構在前文 Redis 設計與實現 3:字符串 SDS 中有講到。
sdshdr 結構大致如下:

redisObject 中的 ptr 指針,就是指向 sds。

編碼 2:embstr
embstr 編碼是專門用於保存短字符串的一種優化編碼方式。當字符串的長度小於等於 44 的時候,將采用 embstr 編碼。
創建字符串對象的代碼如下(object.c):
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
embstr 有個顯著的特點,就是 redisObject 跟 sds 的內存是挨在一起的。挨在一起的好處:
- 分配內存的時候,只需要分配一次。而
raw編碼的sds跟redisObject分離,就要分配兩次內存。 - 同樣,釋放內存也只需要釋放一次。
- 連續內存能更好利用內存帶來的優勢。
其結構示意圖如下:

embstr 問題一:那么為什么 embstr 跟 raw 的界限是 44 呢?
embstr的sds使用了sdshdr8,sdshdr8頭占用了 3 個字節:
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 1 字節 */
uint8_t alloc; /* 1 字節 */
unsigned char flags; /* 1 字節 */
char buf[];
};
- 另外還有
redisObject占用 16 個字節 (4 + 4 + 24 + 32 + 64 = 128位):
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; // #define LRU_BITS 24
int refcount; // 32 位
void *ptr; // 64 位
} robj;
redisObject + sdshdr8 至少需要 3 + 16 = 19 字節。
redis 認為如果超過 64 字節就是大字符串,所以在 redisObject+ sdshdr8 的總長度是 64 字節的情況下,留給 buf 的長度就只剩下 45 字節,由於字符串結尾需要一個 \0 占用一個字節,所以留個字符串的長度就只有 44 字節了。
公式:64 - 3(sdshdr8) - 16(redisObject) - 1(\0) = 44
embstr 問題二:為什么網上有的博文說 embstr 跟 raw 的界限是 39
在 redis 3.2 版本之前,這個界限的確是 39,為什么后面改成 44 了呢?
那是因為 sdshdr 的結構在 3.2 版本的時候修改了。3.2 之前的 sdshdr 結構是:
struct sdshdr {
unsigned int len; // 4 字節
unsigned int free; // 4 字節
char buf[];
};
舊版本的 sdshdr 的頭占用了 8 個字節,比新版本的多了 5 個字節,所以界限就是 44 - 5 = 39 啦!
編碼 3:int
如果一個字符串對象保存的是整數值,並且這個整數值可以用 long 類型來表示,那么這個整數值將會保存在字符串對象結構的 ptr 屬性里面(將 void* 轉換成 long),並將字符串對象的編碼設置為 int。
相對於用 raw 編碼,int 編碼既節省了指針占用的內存,也節省了sds結構的內存。
redis> SET int_key 12345
OK
redis> OBJECT ENCODING int_key
"int"
下圖為存着 12345 的 string 示例結構:

二、編碼的轉換
1. int 轉 raw
- 當字符串傳的不是整數的時候,int 就會轉成 raw 編碼。
- 如果執行了一些修改的命令,如
append等(set不算),都會轉成raw編碼。因為這些操作只有字符串才支持。 - 一旦編碼變為
raw之后,將不會再轉成embstr
127.0.0.1:6379> SET num 1
OK
127.0.0.1:6379> OBJECT ENCODING num
"int"
127.0.0.1:6379> APPEND num 2
(integer) 2
127.0.0.1:6379> OBJECT ENCODING num
"raw"
127.0.0.1:6379> SET num 12
OK
127.0.0.1:6379> OBJECT ENCODING num
"int"
2. embstr 轉 raw
- 如果執行了一些修改的命令,如
append等,都會轉成raw編碼,不管修改后字符串的長度。因為沒有給embstr編碼實現修改接口,所以實際上embsr是只讀的。 - 一旦編碼變為
raw之后,將不會再轉成embstr
三、重點回顧
- 字符串對象有三種編碼,
raw、embstr、int raw負責保存長字符串;embstr負責保存短字符串;int負責保存整數。int和embstr在修改的時候,會轉成raw編碼,並且不再轉回
本文的分析沒有特殊說明都是基於 Redis 6.0 版本源碼
redis 6.0 源碼:https://github.com/redis/redis/tree/6.0
