1. 程式人生 > >Redis 設計與實現 6:五大資料型別之字串

Redis 設計與實現 6:五大資料型別之字串

前文 [Redis 設計與實現 2:Redis 物件](https://www.cnblogs.com/chenchuxin/p/14187921.html) 說到,五大資料型別都會封裝成 `RedisObject`。 ```c typedef struct redisObject { unsigned type:4; // 型別 unsigned encoding:4; // 編碼 // ... void *ptr; // 指向具體底層資料的指標 } robj; ``` 不同資料型別的主要區別就是 `type` 和 `encoding` 屬性的差異,同一種資料型別,有不同的編碼。 # 一、編碼型別 字串的編碼有`raw`、`embstr`、`int`三種。 - `raw` 用於長字串。 - `embstr` 用於短字串。 - `int` 用於整數型別。 定義在 `server.h` 中,這裡只列出 `string` 型別的編碼 ```c #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](https://www.cnblogs.com/chenchuxin/p/14189925.html) 中有講到。 `sdshdr` 結構大致如下: ![sdshdr 結構](https://img-blog.csdnimg.cn/20201228112917616.png) redisObject 中的 `ptr` 指標,就是指向 `sds`。 ![string raw 編碼結構示例](https://img-blog.csdnimg.cn/20201228213902420.png) ## 編碼 2:embstr `embstr` 編碼是專門用於儲存**短字串**的一種優化編碼方式。當字串的長度小於等於 **44** 的時候,將採用 `embstr` 編碼。 建立字串物件的程式碼如下(`object.c`): ```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`分離,就要分配兩次記憶體。 - 同樣,釋放記憶體也只需要釋放一次。 - 連續記憶體能更好利用記憶體帶來的優勢。 其結構示意圖如下: ![string embstr 編碼示意圖](https://img-blog.csdnimg.cn/20201229121012437.png) **embstr 問題一:那麼為什麼 embstr 跟 raw 的界限是 **44** 呢?** - `embstr` 的`sds`使用了 `sdshdr8`,`sdshdr8` 頭佔用了 3 個位元組: ```c 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` 位): ```c 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` 結構是: ```c 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` 示例結構: ![string int 編碼結構示例](https://img-blog.csdnimg.cn/20201228105945950.png) # 二、編碼的轉換 ## 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/