前文 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