1. 程式人生 > >Redis 儲存結構設計

Redis 儲存結構設計

Base 2.8.7
Redis是一個包含了很多Key-Value對的大字典,這個字典支援的Value非常豐富,可以為字串、雜湊表、列表、集合和有序集,基於這些型別豐富的value,擴展出了功能強大的操作,例如hmset、lpush、sadd等

字典

字典是Redis最基礎的資料結構,一個字典即一個DB,Redis支援多DB

Redis字典採用Hash表實現,針對碰撞問題,其採用的方法為鏈地址法,即將多個雜湊值相同的節點串連在一起, 從而解決衝突問題。
“鏈地址法”的問題在於當碰撞劇烈時,效能退化嚴重,例如:當有n個數據,m個槽位,如果m=1,則整個Hash表退化為連結串列,查詢複雜度O(n)
為了避免Hash碰撞攻擊,Redis隨機化了Hash表種子


Redis的方案是“雙buffer”,正常流程使用一個buffer,當發現碰撞劇烈(判斷依據為當前槽位數和Key數的對比),分配一個更大的buffer,然後逐步將資料從老的buffer遷移到新的buffer。

Redis字典結構如下:
typedef struct dict {
    dictType *type; 
    void *privdata; 
    dictht ht[2]; //雙buffer
    int rehashidx; 
    int iterators;
} dict;

typedef struct dictht {
    dictEntry **table; //hash連結串列
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

//資料節點<K,V>
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    struct dictEntry *next;
} dictEntry;
redisObject是真正儲存redis各種型別的結構,其內容如下:
typedef struct redisObject {
    unsigned type:4; //邏輯型別
    unsigned notused:2;     /* Not used */
    unsigned encoding:4; //物理儲存型別
    unsigned lru:22;        /* lru time (relative to server.lruclock) */
    int refcount;
    void *ptr;  //具體資料
} robj;
其中type即redis支援的邏輯型別,包括:
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
enconding為物理儲存方式,一種邏輯型別可以使用不同的儲存方式,包括:
#define REDIS_ENCODING_RAW 0     /* Raw representation */
#define REDIS_ENCODING_INT 1     /* Encoded as integer */
#define REDIS_ENCODING_HT 2      /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6  /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7  /* Encoded as skiplist */

字串

Redis的所有的key都採用字串儲存,另外,Redis也支援字串型別的value。
字串型別即前文中看到的REDIS_STRING,其物理實現(enconding)可以為 REDIS_ENCODING_INT或REDIS_ENCODING_RAW 
REDIS_ENCODING_INT儲存為long型,即redis會嘗試將一個字串轉化為Long,可以轉換的話,即儲存為REDIS_ENCODING_INT
否則,Redis會將REDIS_STRING儲存為字串型別,即REDIS_ENCODING_RAW
字串型別在redis中用sds封裝,主要為了解決長度計算和追加效率的問題,其定義如下:
typedef char *sds;
struct sdshdr {
    int len; // buf 已佔用長度 
    int free; // buf 剩餘可用長度  
    char buf[];// 柔性陣列,實際儲存字串資料的地方
};
static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}
static inline size_t sdsavail(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->free;
}
有時間的同學可以詳細看下Sds.h和Sds.c兩個檔案,還是很有意思的。

Hash表

Redis支援Value為Hash表,其邏輯型別為REDIS_HASH,REDIS_HASH可以有兩種encoding方式: REDIS_ENCODING_ZIPLIST 和 REDIS_ENCODING_HT
REDIS_ENCODING_HT即前文提到的字典的實現
REDIS_ENCODING_ZIPLIST即ZIPLIST,是一種雙端列表,且通過特殊的格式定義,壓縮記憶體適用,以時間換空間。ZIPLIST適合小資料量的讀場景,不適合大資料量的多寫/刪除場景
Hash表預設的編碼格式為REDIS_ENCODING_ZIPLIST,在收到來自使用者的插入資料的命令時:
1,呼叫hashTypeTryConversion函式檢查鍵/值的長度大於 配置的hash_max_ziplist_value(預設64)
2,呼叫hashTypeSet判斷節點數量大於 配置的hash_max_ziplist_entries (預設512)

以上任意條件滿足則將Hash表的資料結構從REDIS_ENCODING_ZIPLIST轉為REDIS_ENCODING_HT

列表

Redis支援Value為一個列表,其邏輯型別為REDIS_SET,REDIS_SET有兩種encoding方式,REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLIST
REDIS_ENCODING_ZIPLIST同上
REDIS_ENCODING_LINKEDLIST是比較正統雙端連結表的實現:
typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;
列表的預設編碼格式為REDIS_ENCODING_ZIPLIST,當滿足以下條件時,編碼格式轉換為REDIS_ENCODING_LINKEDLIST
1,元素大小大於list-max-ziplist-value(預設64)
2,元素個數大於 配置的list-max-ziplist-entries(預設512)

集合

Redis支援Value為集合,其邏輯型別為REDIS_SET,REDIS_SET有兩種encoding方式: REDIS_ENCODING_INTSET 和 REDIS_ENCODING_HT(同上)
集合的元素型別和數量決定了encoding方式,預設採用REDIS_ENCODING_INTSET ,當滿足以下條件時,轉換為REDIS_ENCODING_HT:
1. 元素型別不是整數
2. 元素個數超過配置的“set-max-intset-entries”(預設512)
REDIS_ENCODING_INTSET是一個有序陣列,使用的資料結構如下:
typedef struct intset {
    uint32_t encoding;  //3種類型:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64
    uint32_t length; //元素個數
    int8_t contents[]; //元素實際存放的位置,按序排放
} intset;
Redis會根據整數大小選擇最適合的型別,當發生變更時,進行調整

有序集

Redis支援Value為有序集合,其邏輯型別為REDIS_ZSET,REDIS_ZSET有兩種encoding方式: REDIS_ENCODING_ZIPLIST(同上)和 REDIS_ENCODING_SKIPLIST
REDIS_ENCODING_SKIPLIST使用的資料結構如下,其同事:

typedef struct zset {
    dict *dict;  //Hash字典(同前文)
    zskiplist *zsl; //跳躍表
} zset;
由於有續集每一個元素包括:<member,score>兩個屬性,為了保證對member和score都有很好的查詢效能,REDIS_ENCODING_SKIPLIST同時採用字典和有序集兩種資料結構來儲存資料元素。字典和有序集通過指標指向同一個資料節點來避免資料冗餘。
字典中使用member作為key,score作為value,從而保證在O(1)時間對member的查詢
跳躍表基於score做排序,從而保證在 O(logN) 時間內完成通過score對memer的查詢
有續集預設也是採用REDIS_ENCODING_ZIPLIST的實現,當滿足以下條件時,轉換為REDIS_ENCODING_SKIPLIST
1. 資料元素個數超過配置的zset_max_ziplist_entries 的值(預設值為 128 )
2. 新新增元素的 member 的長度大於配置的 zset_max_ziplist_value 的值(預設值為 64 )

總結

針對同一種資料型別,Redis會根據元素型別/大小/個數採用不同的編碼方式,不同的編碼方式在記憶體使用效率/查詢效率上差距巨大,在遇到記憶體問題時,可以嘗試下修改相關引數:
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64