Redis 設計與實現 6:五大資料型別之字串
阿新 • • 發佈:2020-12-29
前文 [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/