Redis 的 9 種資料型別
本文GitHub已收錄:https://zhouwenxing.github.io/
Redis
中支援的資料型別到 5.0.5
版本,一共有 9
種。分別是:
- 1、Binary-safe strings(二進位制安全字串)
- 2、Lists(列表)
- 3、Sets(集合)
- 4、Sorted sets(有序集合)
- 5、Hashes(雜湊)
- 6、Bit arrays (or simply bitmaps)(點陣圖)
- 7、HyperLogLogs
- 8、 geospatial
- 9、Streams
雖然這裡列出了 9
種,但是基礎型別就是前面 5
種。後面的 4
種是基於前面 5
種基本型別及特定的演算法來實現的特殊型別。
而在 5
種基礎型別之中,又尤其以字串型別最為常用,且 key
值只能為字串物件,所以要想深入的瞭解 Redis
的特性,字串物件是首先需要學習的。
五種基本資料型別之字串物件
Redis
當中有五種基礎資料型別,而字串物件又是最重要最常用的一種型別。
二進位制安全字串
Redis
是基於 C
語言進行開發的,而 C
語言中的字串是二進位制不安全的,所以 Redis
就沒有直接使用 C
語言的字串,而是自己編寫了一個新的資料結構來表示字串,這種資料結構稱之為:簡單動態字串(Simple dynamic string),簡稱 SDS
。
什麼是二進位制安全的字串
在 C
語言中,字串採用的是一個 char
陣列(柔性陣列)來儲存字串,而且字串必須要以一個空字串 \0
來結尾。而且字串並不記錄長度,所以如果想要獲取一個字串的長度就必須遍歷整個字串,直到遇到第一個 \0
為止(\0
不會計入字串長度),故而獲取字串長度的時間複雜度為 O(n)
。
正因為 C
語言中是以遇到的第一個空字元 \0
來識別是否到了字串末尾,因此其只能儲存文字資料,不能儲存圖片,音訊,視訊和壓縮檔案等二進位制資料,否則可能出現字串不完整的問題,所以其是二進位制不安全的。
Redis
中為了實現二進位制安全的字串,對原有 C
語言中的字串實現做了改進。如下所示就是一箇舊版本的 sds
字串的結構定義:
struct sdshdr{
int len;//記錄buf陣列已使用的長度,即SDS的長度(不包含末尾的'\0')
int free;//記錄buf陣列中未使用的長度
char buf[];//位元組陣列,用來儲存字串
}
經過改進之後,如果想要獲取 sds
的長度不用去遍歷 buf
陣列了,直接讀取 len
屬性就可以得到長度,時間複雜度一下就變成了 O(1)
,而且因為判斷字串長度不再依賴空字元 \0
,所以其能儲存圖片,音訊,視訊和壓縮檔案等二進位制資料,不用擔心讀取到的字串不完整。
需要注意的是,sds
依然遵循了 C
語言字串以 \0
結尾的慣例,這麼做是為了方便複用 C
語言字串原生的一些API,換言之就是在 C
語言中會以碰到的第一個 \0
字元當做當前字串物件的結尾,所以如果一些二進位制資料就會可能出現讀取字串不完整的現象,而 sds
會以長度來判斷是否到字串末尾。
在 Redis 3.2
之後的版本,Redis
對 sds
又做了優化,按照儲存空間的大小拆分成為了 sdshdr5
、sdshdr8
、sdshdr16
、sdshdr32
、sdshdr64
,分別用來儲存大小為:32
位元組(2
的 5
次方),256
位元組(2
的 8
次方),64KB
(2
的 16
次方),4GB
大小(2
的 32
次方)以及 2
的 64
次方大小的字串(因為目前版本 key
和 value
都限制了最大 512MB
,所以 sdshdr64
暫時並未使用到)。 sdshdr5
只被應用在了 Redis
中的 key
中,value
中不會被使用到,因為sdshdr5
和其他型別也不一樣,其並沒有儲存未使用空間,所以其是比較適用於使用大小固定的場景(比如 key
值):
任意選擇其中一種資料型別,其欄位代表含義如下:
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; //已使用空間大小
uint8_t alloc; //總共申請的空間大小(包括未使用的)
unsigned char flags; //用來表示當前sds型別是sdshdr8還是sdshdr16等
char buf[]; //真實儲存字串的位元組陣列
};
可以看到相比較於 Redis 3.2
版本之前的 sds
主要是修改了 free
屬性然後新增了一個 flags
標記來區分當前的 sds
型別。
sds 空間分配策略
C
語言中因為字串內部沒有記錄長度,所以如果擴充字串的時候非常容易造成緩衝區溢位(buffer overflow)。
請看下面這張圖,假設下面這張圖就是記憶體裡面的連續空間,可以很明顯的看到,此時 wolf
和 Redis
兩個字串之間只有三個空位,那麼這時候如果我們要將 wolf
字串修改為 lonelyWolf
,那麼就需要 6
個空間,這時候下面這個空間是放不下的,所以必須要重新申請空間,但是假如說程式設計師忘了申請空間,或者說申請到的空間依然不夠,那麼就會出現後面的 Redis
字串中的 Red
被覆蓋了:
同樣的,假如要縮小字串的長度,那麼也需要重新申請釋放記憶體。否則,字串一直佔據著未使用的空間,會造成記憶體洩露。
C
語言避免快取區溢位和記憶體洩露完全依賴於人為,很難把控,但是使用 sds
就不會出現這兩個問題,因為當我們操作 sds
時,其內部會自動執行空間分配策略,從而避免了上述兩種情況的出現。
空間預分配
空間預分配指的是當我們通過 api
對 sds
進行擴充套件空間的時候,假如未使用空間不夠用,那麼程式不僅會為 sds
分配必須要的空間,還會額外分配未使用空間,未使用空間分配大小主要有兩種情況:
- 1、假如擴大長度之後的
len
屬性小於等於1MB
(即 1024 * 1024),那麼就會同時分配和len
屬性一樣大小的未使用空間(此時buf
陣列已使用空間 = 未使用空間)。 - 2、假如擴大長度之後的
len
屬性大於1MB
,那麼就會分配1MB
未使用空間大小。
執行空間預分配策略的好處是提前分配了未使用空間備用後,就不需要每次增大字串都需要分配空間,減少了記憶體重分配的次數。
惰性空間釋放
惰性空間釋放指的是當我們需要通過 api
減小 sds
長度的時候,程式並不會立即釋放未使用的空間,而只是更新 free
屬性的值,這樣空間就可以留給下一次使用。而為了防止出現記憶體溢位的情況,sds
單獨提供給了 api
讓我們在有需要的時候去真正的釋放記憶體。
sds 和 C 語言字串區別
下面表格中列舉了 Redis
中的 sds
和 C
語言中實現的字串的區別:
C 字串 | SDS |
---|---|
只能儲存文字類不含空字串 \0 資料 |
可以儲存文字或者二進位制資料,允許包含空字串 \0 |
獲取字串長度的複雜度為 O(n) |
獲取字串長度的複雜度為 O(1) |
操作字串可能會造成緩衝區溢位 | 不會出現緩衝區溢位情況 |
修改字串長度 N 次,必然需要 N 次記憶體重分配 |
修改字串長度 N 次,最多需要 N 次記憶體重分配 |
可以使用 C 字串相關的所有函式 |
可以使用 C 字串相關的部分函式 |
sds 是如何被儲存的
在 Redis
中所有的資料型別都是將對應的資料結構再進行了再一次包裝,建立了一個字典物件來儲存的,sds
也不例外。每次建立一個 key-value
鍵值對,Redis
都會建立兩個物件,一個是鍵物件,一個是值物件。而且需要注意的是在 Redis
中,值物件並不是直接儲存,而是被包裝成 redisObject
物件,並同時將鍵物件和值物件通過 dictEntry
物件進行封裝,如下就是一個 dictEntry
物件:
typedef struct dictEntry {
void *key;//指向key,即sds
union {
void *val;//指向value
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;//指向下一個key-value鍵值對(雜湊值相同的鍵值對會形成一個連結串列,從而解決雜湊衝突問題)
} dictEntry;
redisObject
物件的定義為:
typedef struct redisObject {
unsigned type:4;//物件型別(4位=0.5位元組)
unsigned encoding:4;//編碼(4位=0.5位元組)
unsigned lru:LRU_BITS;//記錄物件最後一次被應用程式訪問的時間(24位=3位元組)
int refcount;//引用計數。等於0時表示可以被垃圾回收(32位=4位元組)
void *ptr;//指向底層實際的資料儲存結構,如:sds等(8位元組)
} robj;
當我們在 Redis
客戶端中執行命令 set name lonely_wolf
,就會得到下圖所示的一個結構(省略了部分屬性):
看到這個圖想必大家會有疑問,這裡面的 type
和 encoding
到底是什麼呢?其實這兩個屬性非常關鍵,Redis
就是通過這兩個屬性來識別當前的 value
到底屬於哪一種基本資料型別,以及當前資料型別的底層採用了何種資料結構進行儲存。
type 屬性
type
屬性表示物件型別,其對應了 Redis
當中的 5
種基本資料型別:
型別屬性 | 描述 | type 命令返回值 |
---|---|---|
REDIS_STRING | 字串物件 | string |
REDIS_LIST | 列表物件 | list |
REDIS_HASH | 雜湊物件 | hash |
REDIS_SET | 集合物件 | set |
REDIS_ZSET | 有序集合物件 | zset |
可以看到,這就是對應了我們 5
種常用的基本資料型別。
encoding 屬性
Redis
當中每種資料型別都是經過特別設計的,相信大家看完這個系列也會體會到 Redis
設計的精妙之處。字串在我們眼裡是非常簡單的一種資料結構了,但是 Redis
卻把它優化到了極致,為了節省空間,其通過編碼的方式定義了三種不同的儲存方式:
編碼屬性 | 描述 | object encoding命令返回值 |
---|---|---|
OBJ_ENCODING_INT | 使用整數的字串物件 | int |
OBJ_ENCODING_EMBSTR | 使用 embstr 編碼實現的字串物件 |
embstr |
OBJ_ENCODING_RAW | 使用 raw 編碼實現的字串物件 |
raw |
int
編碼
當我們用字串物件儲存的是整型,且能用8
個位元組的long
型別進行表示(即2
的63
次方減1
),則Redis
會選擇使用int
編碼來儲存,此時redisObject
物件中的ptr
指標直接替換為long
型別。我們想想8
個位元組如果用字串來儲存只能存8
位,也就是千萬級別的數字,遠遠達不到2
的63
次方減1
這個級別,所以如果都是數字,用long
型別會更節省空間。embstr
編碼
當字串物件中儲存的是字串,且長度小於44
(Redis 3.2
版本之前是39
)時,Redis
會選擇使用embstr
編碼來儲存。raw
編碼
當字串物件中儲存的是字串,且長度大於44
時,Redis
會選擇使用raw
編碼來儲存。
講了半天理論,接下來讓我們一起來驗證下這些結論,依次輸入 set name lonely_wolf
,type name
,object encoding name
命令:
可以發現當前的資料型別就是 string
,普通字串因為長度小於 44
,所以採用的是 embstr
編碼。
再依次輸入:set num 1111111111
,set address aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(長度 44
),set address aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(長度 45
),分別檢視型別和編碼:
可以發現,當輸入純數字的時候,採用的是 int
編碼,而字串小於等於 44
則為 embstr
,大於 44
則為 raw
編碼。
字串物件中除了上面提到的純整數和字串,還可以儲存浮點型型別,所以字串物件可以儲存以下三種型別:
- 字串
- 整數
- 浮點數
而當我們的 value
為整數時,還可以使用原子自增命令來實現 value
的自增,這個命令在實際開發過程中非常實用。
incr
:自增1
。incrby
:自增指定數值。
不過這兩個命令只能用在 value
為整數的場景,當 value
不是整數時則會報錯。
embstr 編碼為什麼從 39 位修改為 44 位
embstr
編碼中,redisObject
和 sds
是連續的一塊記憶體空間,這塊記憶體空間 Redis
限制為了 64
個位元組,而redisObject
固定佔了16位元組(上面定義中有標註),Redis 3.2
版本之前的 sds
佔了 8
個位元組,再加上字串末尾 \0
佔用了 1
個位元組,所以:64-16-8-1=39
位元組。
Redis 3.2
版本之後 sds
做了優化,對於 embstr
編碼會採用 sdshdr8
來儲存,而 sdshdr8
佔用的空間只有 24
位:3
位元組(len+alloc+flag)+ \0
字元(1位元組),所以最後就剩下了:64-16-3-1=44
位元組。
embstr 編碼和 raw 編碼的區別
embstr
編碼是一種優化的儲存方式,其在申請空間的時候因為 redisObject
和 sds
兩個物件是一個連續空間,所以只需要申請 1
次空間(同樣的,釋放記憶體也只需要 1
次),而 raw
編碼因為 redisObject
和 sds
兩個物件的空間是不連續的,所以使用的時候需要申請 2
次空間(同樣的,釋放記憶體也需要 2
次)。但是使用 embstr
編碼時,假如需要修改字串,那麼因為 redisObject
和 sds
是在一起的,所以兩個物件都需要重新申請空間,為了避免這種情況發生,embstr
編碼的字串是隻讀的,不允許修改。
上圖中的示例我們看到,對一個 embstr
編碼的字串物件進行 append
操作時,長度還沒有達到 45
,但是編碼已經被修改為 raw
了,這就是因為 embstr
編碼是隻讀的,如果需要對其修改,Redis
內部會將其修改為 raw
編碼之後再操作。同樣的,如果是操作 int
編碼的字串之後,導致 long
型別無法儲存時(int
型別不再是整數或者長度超過 2
的 63
次方減 1
時),也會將 int
編碼修改為 raw
編碼。
PS:需要注意的是,編碼一旦升級(int-->embstr-->raw),即使後期再把字串修改為符合原編碼能儲存的格式時,編碼也不會回退。
總結
本文主要講述了 Redis
當中最常用的字元創物件,通過二進位制安全字串的特別逐步分析了 sds
的底層儲存即編碼格式,並分別介紹了每種編碼格式的區別,最後通過示例來演示了編碼的轉換過程。