Redis 存儲原理(1)

Redis如今基本也算是後臺開發的基礎服務,基本像Mysql同樣廣泛在應用中使用了。我第一次接觸的Nosql是memcache用來解決誇服務session共享問題。後來由於memcache沒法持久化問題改成使用Redis。此次主要針對Redis作一個整理。c++

Redis數據類型

類型 特色說明
String 類型是 Redis 最基本的數據類型,string 類型的值最大能存儲 512MB
Hash Redis hash 是一個 string 類型的 field 和 value 的映射表,hash 特別適合用於存儲對象。
List Redis 列表是簡單的字符串列表,按照插入順序排序。你能夠添加一個元素到列表的頭部(左邊)或者尾部(右邊)
Set Redis 的 Set 是 string 類型的無序集合。集合是經過哈希表實現的,因此添加,刪除,查找的複雜度都是 O(1)
ZSet 與Set不一樣的是每一個元素都會關聯一個double類型的分數。redis正是經過分數來爲集合中的成員進行從小到大的排序
HyperLogLog 在 2.8.9 版本添加是用來作基數統計的算法,HyperLogLog 的優勢是,在輸入元素的數量或者體積很是很是大時,計算基數所需的空間老是固定 的、而且是很小的
Bitmaps 可作爲布隆過濾器使用
GeoHash Redis 3.2 版本地理空間位置(緯度、經度、名稱)

存儲(實現)原理

數據模型

以set k1 hello爲例,由於Redis是KV的數據庫,它是經過hashtable實現的(咱們把這個叫作外層的哈希)。因此每一個鍵值對都會有一個dictEntry(源碼位置:dict.h),裏面指向了key和value的指針。next指向下一個dictEntry。redis

typedef struct dictEntry {
    void *key; /* Key關鍵字定義 */
    union {
        void *val;  /* value定義 */
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;/*下一個節點*/
} dictEntry;

key是字符串,可是Redis沒有直接使用C的字符數組,而是存儲在自定義的SDS中。算法

value既不是直接做爲字符串存儲,也不是直接存儲在SDS中,而是存儲在redisObject中。實際上五種經常使用的數據類型的任何一種,都是經過redisObject來存儲的。sql

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位,對象最後一次被命令程序訪問的時間,與內存回收有關*/
                           /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;/*引用計數。當refcount爲0的時候,表示該對象已經不被任何對象引用,則能夠進行垃圾回收了*/
    void *ptr;/*指向對象實際的數據結構*/
} robj;
127.0.0.1:6379>set num1 1 
OK
127.0.0.1:6379>set str1 "aaaadddddddddddddddddddddddddddddddccccccccccccccccccc"
OK
127.0.0.1:6379>set str2 beijing
OK
127.0.0.1:6379>object encoding num1
"int"
127.0.0.1:6379>object encoding str1
"embstr"
127.0.0.1:6379>object encoding str2 
"raw

字符串類型的內部編碼有三種:數組

一、int,存儲8個字節的長整型(long,2^63-1)。安全

二、embstr,表明embstr格式的SDS(SimpleDynamicString簡單動態字符串),存儲小於44個字節的字符串。session

三、raw,存儲大於44個字節的字符串(3.2版本以前是39字節)。數據結構

/* <object.c>
 * Create a string object with EMBSTR encoding if it is smaller than
 * OBJ_ENCODING_EMBSTR_SIZE_LIMIT, otherwise the RAW encoding is
 * used.
 *
 * The current limit of 44 is chosen so that the biggest string object
 * we allocate as EMBSTR will still fit into the 64 byte arena of jemalloc. */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
什麼是SDS?

Redis中字符串的實現。在3.2之後的版本中,SDS又有多種結構(sds.h):sdshdr五、sdshdr八、sdshdr1六、sdshdr3二、sdshdr64,用於存儲不一樣的長度的字符串,分別表明:
2^5=32byte
2^8=256byte
2^16=65536byte=64KB
2^32byte=4GB。app

爲何Redis要用SDS實現字符串?

C語言自己沒有字符串類型(只能用字符數組char[]實現)。

一、使用字符數組必須先給目標變量分配足夠的空間,不然可能會溢出。

二、若是要獲取字符長度,必須遍歷字符數組,時間複雜度是O(n)。

三、C字符串長度的變動會對字符數組作內存重分配。

四、經過從字符串開始到結尾碰到的第一個'\0'來標記字符串的結束,所以不能保存圖片、音頻、視頻、壓縮文件等二進制(bytes)保存的內容,二進制不安全。

SDS的特色:

一、不用擔憂內存溢出問題,若是須要會對SDS進行擴容。

二、獲取字符串長度時間複雜度爲O(1),由於定義了len屬性。

三、經過「空間預分配」(sdsMakeRoomFor)和「惰性空間釋放」,防止屢次重分配內存。

四、判斷是否結束的標誌是len屬性(它一樣以'\0'結尾是由於這樣就可使用C語言中函數庫操做字符串的函數了),能夠包含'\0'。

c字符串 SDS
embstr 只讀
獲取字符串長度的複雜度爲O(N) 獲取字符串長度的複雜度爲O(1)
API是不安全的,可能會形成緩衝區溢出 API是安全的,不會早晨個緩衝區溢出
修改字符串長度N次必然須要執行N次內存重分配 修改字符串長度N次最多須要執行N次內存重分配
只能保存文本數據 能夠保存文本或者二進制數據
可使用全部<string.h>庫中的函數 可使用一部分<string.h>庫中的函數
embstr和raw的區別?

embstr的使用只分配一次內存空間(由於RedisObject和SDS是連續的),而raw須要分配兩次內存空間(分別爲RedisObject和SDS分配空間)。

所以與raw相比,embstr的好處在於建立時少分配一次空間,刪除時少釋放一次空間,以及對象的全部數據連在一塊兒,尋找方便。

而embstr的壞處也很明顯,若是字符串的長度增長鬚要從新分配內存時,整個RedisObject和SDS都須要從新分配空間,所以Redis中的embstr實現爲只讀。

int和embstr何時轉化爲raw?

當int數據再也不是整數,或大小超過了long的範圍(2^631=9223372036854775807)時,自動轉化爲embstr。

127.0.0.1:6379>set s1 1 
OK
127.0.0.1:6379>append s1 a
(integer)2
127.0.0.1:6379>object encoding s1
"raw"
明明沒有超過閾值,爲何變成raw了?
127.0.0.1:6379>set s2 a 
OK
127.0.0.1:6379>object encoding s2
"embstr"
127.0.0.1:6379>append s2 b
(integer)2
127.0.0.1:6379>object encoding s2
"raw"

對於embstr,因爲其實現是隻讀的,所以在對embstr對象進行修改時,都會先轉化爲raw再進行修改。

所以,只要是修改embstr對象,修改後的對象必定是raw的,不管是否達到了44個字節。

當長度小於閾值時,會還原嗎?

關於Redis內部編碼的轉換,都符合如下規律:編碼轉換在Redis寫入數據時完成,且轉換過程不可逆,只能從小內存編碼向大內存編碼轉換(可是不包括從新set)

爲何要對底層的數據結構進行一層包裝呢?

經過封裝,能夠根據對象的類型動態地選擇存儲結構和可使用的命令,實現節省空間和優化查詢速度。

相關文章
相關標籤/搜索