Redis存储(实现)原理
數據模型
set hello word 為例,因為Redis 是KV 的數據庫,它是通過hashtable 實現的(我們把這個叫做外層的哈希)。所以每個鍵值對都會有一個dictEntry(源碼位置:dict.h),里面指向了key 和value 的指針。next 指向下一個dictEntry。
typedef struct dictEntry {void *key; /* key 關鍵字定義*/union {void *val; uint64_t u64; /* value 定義*/int64_t s64; double d;} v;struct dictEntry *next; /* 指向下一個鍵值對節點*/ } dictEntry;key 是字符串,但是Redis 沒有直接使用C 的字符數組,而是存儲在自定義的SDS中。
value 既不是直接作為字符串存儲,也不是直接存儲在SDS 中,而是存儲在redisObject 中。實際上五種常用的數據類型的任何一種,都是通過redisObject 來存儲的。
redisObject
redisObject 定義在src/server.h 文件中。
typedef struct redisObject {unsigned type:4; /* 對象的類型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */unsigned encoding:4; /* 具體的數據結構*/unsigned lru:LRU_BITS; /* 24 位,對象最后一次被命令程序訪問的時間,與內存回收有關*/int refcount; /* 引用計數。當refcount 為0 的時候,表示該對象已經不被任何對象引用,則可以進行垃圾回收了*/void *ptr; /* 指向對象實際的數據結構*/ } robj;可以使用type 命令來查看對外的類型。
127.0.0.1:6379> type qs string 127.0.0.1:6379> set number 1 OK 127.0.0.1:6379> set qs "is a good teacher in gupao, have crossed mountains and sea " OK 127.0.0.1:6379> set jack bighead OK 127.0.0.1:6379> object encoding number "int" 127.0.0.1:6379> object encoding jack "embstr" 127.0.0.1:6379> object encoding qs "raw"字符串類型的內部編碼有三種:
1、int,存儲8 個字節的長整型(long,2^63-1)。
2、embstr, 代表embstr 格式的SDS(Simple Dynamic String 簡單動態字符串),存儲小于44 個字節的字符串。
3、raw,存儲大于44 個字節的字符串(3.2 版本之前是39 字節)。為什么是39?
/* object.c */ #define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44問題1、什么是SDS?
Redis 中字符串的實現。
在3.2 以后的版本中,SDS 又有多種結構(sds.h):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存儲不同的長度的字符串,分別代表2^5=32byte,2^8=256byte,2^16=65536byte=64KB,2^32byte=4GB。
/* sds.h */ struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* 當前字符數組的長度*/ uint8_t alloc; /*當前字符數組總共分配的內存大小*/ unsigned char flags; /* 當前字符數組的屬性、用來標識到底是sdshdr8 還是sdshdr16 等*/ char buf[]; /* 字符串真正的值*/ };問題2、為什么Redis 要用SDS 實現字符串?
我們知道,C 語言本身沒有字符串類型(只能用字符數組char[]實現)。
1、使用字符數組必須先給目標變量分配足夠的空間,否則可能會溢出。
2、如果要獲取字符長度,必須遍歷字符數組,時間復雜度是O(n)。
3、C 字符串長度的變更會對字符數組做內存重分配。
4、通過從字符串開始到結尾碰到的第一個'\0'來標記字符串的結束,因此不能保存圖片、音頻、視頻、壓縮文件等二進制(bytes)保存的內容,二進制不安全。
SDS 的特點:
1、不用擔心內存溢出問題,如果需要會對SDS 進行擴容。
2、獲取字符串長度時間復雜度為O(1),因為定義了len 屬性。
3、通過“空間預分配”( sdsMakeRoomFor)和“惰性空間釋放”,防止多次重分配內存。
4、判斷是否結束的標志是len 屬性(它同樣以'\0'結尾是因為這樣就可以使用C語言中函數庫操作字符串的函數了),可以包含'\0'。
?
問題3、embstr 和raw 的區別?
embstr 的使用只分配一次內存空間(因為RedisObject 和SDS 是連續的),而raw需要分配兩次內存空間(分別為RedisObject 和SDS 分配空間)。
因此與raw 相比,embstr 的好處在于創建時少分配一次空間,刪除時少釋放一次空間,以及對象的所有數據連在一起,尋找方便。
而embstr 的壞處也很明顯,如果字符串的長度增加需要重新分配內存時,整個RedisObject 和SDS 都需要重新分配空間,因此Redis 中的embstr 實現為只讀。
問題4:int 和embstr 什么時候轉化為raw?
當int 數據不再是整數, 或大小超過了long 的范圍(2^63-1=9223372036854775807)時,自動轉化為embstr。
127.0.0.1:6379> set k1 1 OK 127.0.0.1:6379> append k1 a (integer) 2 127.0.0.1:6379> object encoding k1 "raw"問題5:明明沒有超過閾值,為什么變成raw 了?
127.0.0.1:6379> set k2 a OK 127.0.0.1:6379> object encoding k2 "embstr" 127.0.0.1:6379> append k2 b (integer) 2 127.0.0.1:6379> object encoding k2 "raw"對于embstr,由于其實現是只讀的,因此在對embstr 對象進行修改時,都會先轉化為raw 再進行修改。
因此,只要是修改embstr 對象,修改后的對象一定是raw 的,無論是否達到了44個字節。
?
問題6:當長度小于閾值時,會還原嗎?
關于Redis 內部編碼的轉換,都符合以下規律:編碼轉換在Redis 寫入數據時完成,且轉換過程不可逆,只能從小內存編碼向大內存編碼轉換(但是不包括重新set)。
?
問題7:為什么要對底層的數據結構進行一層包裝呢?
通過封裝,可以根據對象的類型動態地選擇存儲結構和可以使用的命令,實現節省空間和優化查詢速度。
?
總結
以上是生活随笔為你收集整理的Redis存储(实现)原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis基本数据类型
- 下一篇: Redis Hash 哈希 结构