redis學習筆記之5大數據類型的基本實現

1.內部編碼:

每種數據類型都有不同的編碼,在滿足一定條件下會進行編碼轉換

2.各種編碼對應的數據結構簡單介紹:

A.簡單動態字符串(sds,simple dynamic string):

因傳統C的字符串不能高效支持長度計算與append操作,故redis採用sds替換C默認的字符串表示

傳統C字符串計算長度:strlen(s)複雜度爲O(n)

傳統C字符串n次append:進行n次內存分配(realloc)

sds的實現:

typedef struct sdshdr{

    int len;//buf已用長度

    int free;//buf剩餘可用長度

    char buf[];//實際保存字符串數據的地方

}

因此,sds字符串計算長度複雜度爲O(1);append時候,當free‘小於append字符串長度時進行擴容,減少內存分配(分配機制後面介紹)

B.linkedList雙端鏈表:

typedef struct listNode{                                              typedef struct list{

    struct listNode * prev;//前驅結點                                  listNode *head;//表頭指針

    struct listNode *next;//後繼節點                                   listNode *tail;//表尾指針

    void *value;//節點值                                                    unsigned long len;//節點數量

}listNode;                                                                        void (*dup)(void *ptr);//複製函數

                                                                                       void (*free)(void *ptr);//釋放函數

                                                                                       int (*match)(void *ptr,void *key);//對比函數

                                                                                   }list;

性能特徵:listNode有prev與next兩個指針,可從兩個方向進行迭代;list有head何tail兩個指針,表頭與表尾插入複雜度爲O(1),即lpush、lpop、rpoplpush等命令高效的原因;list帶len屬性,計算鏈表長度的複雜度爲O(1),因此len命令不會成爲性能瓶頸

C.字典(k-v映射):

redis字典用途:實現redis數據庫鍵空間、hash的底層實現

字典的實現:

typedef struct dict {

    dictType *type;// 特定於類型的處理函數

    void *privdata;// 類型處理函數的私有數據

    dictht ht[2];// 哈希表(2個,第1個是主hash表,第2個用於rehash)

    int rehashidx;// 記錄rehash進度的標誌,值爲-1 表示rehash 未進行

    int iterators;// 當前正在運作的安全迭代器數量

} dict;

哈希表的實現:

typedef struct dictht {

    dictEntry **table;// 哈希表節點指針數組(俗稱桶,bucket)

    unsigned long size;// 指針數組的大小

    unsigned long sizemask;// 指針數組的長度掩碼,用於計算索引值

    unsigned long used;// 哈希表現有的節點數量

} dictht;     其中,table屬性爲數組,數組元素是指向dictEntry結構的指針

dictEntry哈希表節點的實現:

typedef struct dictEntry {

    void *key;// 鍵

    union { void *val; uint64_t u64; int64_t s64; } v;// 值

    struct dictEntry *next;// 鏈往後繼節點

} dictEntry;

next屬性指向下一個dictEntry,多個dictEntry通過next連接成鏈表,當多個鍵擁有相同的hash值時,哈希表用鏈地址法(與jdk1.7的HashMap處理方式一樣)來處理鍵衝突,使用鏈表將這些鍵連接起來

因此,整個字典的結構如下:

D.skipList跳躍表:

跳躍表在redis中唯一作用是實現有序集合zset,通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問的目的,增、刪、查等操作可以在對數期望時間下完成,結構圖如下:

    

跳躍表構成:

表頭:負責維護跳躍表的節點指針

表尾:全部由null組成,表示跳躍表的末尾

表節點:保存元素值,及多個層

層:保存指向其他元素的指針,高層指針越過的元素數量總是大於等於底層的指針

跳躍表的實現:

typedef struct zskiplist {

    struct zskiplistNode *header, *tail;// 頭節點,尾節點

    unsigned long length;// 節點數量

    int level;// 目前表內節點的最大層數

} zskiplist;

跳躍表節點的實現:

typedef struct zskiplistNode {

    robj *obj;// member 對象

    double score;// 分值,允許重複,score相等時還要比較member對象

    struct zskiplistNode *backward;// 後退指針,zrevrange等逆序命令用

    struct zskiplistLevel {

        struct zskiplistNode *forward;// 前進指針

        unsigned int span;// 這個層跨越的節點數量

    } level[];// 層

} zskiplistNode;

E.整數集合intset:

用於有序、無重複地保存多個整數值,會根據元素值自動選擇合適長度的整數類型來保存,是集合類型set的底層實現之一,當set只保存整數元素且數量不多時會用intset

intset的實現:

typedef struct intset {

    uintXX_t encoding;// 保存元素所使用的類型的長度

    uintXX_t length;// 元素個數

    intXX_t contents[];// 保存元素的數組,元素不重複且從小到大排序

} intset; (PS:XX可以是16、32、64,添加元素、分配內存由encoding決定)

在添加元素時,若intset當前編碼不適用新元素編碼,會觸發對intset的升級,升級不會改變元素的值,且編碼方式由元素中長度最大的那個決定,不可逆,只能由較短編碼升級到較長編碼,升級會引起intset內存重分配,並移動集合在所有元素,複雜度爲O(n),應儘量保持整數範圍一致,儘量避免因個別大整數觸發升級操作,浪費內存

intset添加元素的執行流程如下:

F.zipList壓縮列表:

ziplist 是由一系列特殊編碼的內存塊構成的列表,可保存字符數組或整數值,是hash、zset、list底層實現之一 分佈結構如下圖:

zlbytes:整個ziplist列表佔用內存數,用於內存重分配、計算末端

zltail:達到ziplist表尾節點的偏移量,可不遍歷整個ziplist便彈出尾節點

zllen:ziplist列表的節點數量,該值<65535時表示節點數量,等於時需遍歷

entryX:ziplist所保存的節點,節點長度視內容而定

zlend:用於標誌ziplist的末端,1字節

zipList節點分佈結構:

pre_entry_length:上一個節點長度,從而可通過指針計算跳轉到上一個節點

encoding:content部分所保存的數據類型,00、01、10表示保存的是字符數組,11表示保存的是整數

length:content所保存數據的長度

content:保存節點內容,長度與類型由encoding與length決定

使用zipList好處就是可以最大程度上節省內存,適合存儲小對象與有限長度數據(512字節以內),但列表長度不可無限制,否則對該列表的操作時間會大大增加,得不償失

G.redis對象的數據結構:

由於redis的鍵值可以保存不同類型的值且每種數據類型又對應多種編碼,因此需要對鍵值類型進行檢查及"多態"處理,比如:lpush只能用於列表鍵,del可用於任何鍵,必須爲不同類型的鍵設置不同的處理方式,因此redis構建了自己的類型系統

redis類型系統的主要功能如下:

a.redisObject的實現:

typedef struct redisObject {

    unsigned type:4;// 類型

    unsigned notused:2;// 對齊位

    unsigned encoding:4;// 編碼方式 unsigned

    lru:22;// LRU 時間

    int refcount;// 引用計數

    void *ptr;// 指向對象的值

} robj;

主要屬性:

type:對象所保存的值的類型,0-String,1-list,2-set,3-zset,4-hash

encoding:對象所保存的值的編碼,0-raw,1-int,2-hashtable,3-zipmap,4-linkedlist,5-ziplist,6-intset,7-skiplist

ptr:指向實際保存值的數據結構,由type與encoding共同決定

lru:記錄對象最後一次被訪問的時間,用於輔助lru算法刪除數據

refcount:當前對象被引用的次數,爲0可安全回收

b.基於redisObject對象的類型檢查

當執行一個處理數據類型的命令時,redis執行以下步驟:

1.根據給定key,在數據庫字典中查找和它像對應的redisObject,如果沒找到,就返回NULL;

2.檢查redisObject的type屬性和執行命令所需的類型是否相符,如果不相符,返回類型錯誤;

3.根據redisObject 的encoding 屬性所指定的編碼,選擇合適的操作函數來處理底層的數據結構;

4.返回數據結構的操作結果作爲命令的返回值

以lpop命令爲例,執行步驟如下圖:

c.共享對象:

redis內部維護一個0到9999的對象池,可通過redis_shared_integers參數配置,當其他類型(如:list、set、zset、hash)的輸入值在該範圍內,則該對象值的指針將指向共享對象(PS:共享對象只能被字典和雙端鏈表這類能帶有指針的數據結構使用,像整數集合和壓縮列表這些只能保存字符串、整數等字面值的內存數據結構,就不能使用共享對象) 對象共享意味着多個引用共享同一個redisObject,這時lru字段也會被共享,導致無法獲取每個對象的最後訪問時間),因此共享對象池與maxmemory+lru策略衝突,使用需注意

d.對redisObject的分配、共享和銷燬機制:

1.每個redisObject 結構都帶有一個refcount 屬性,指示這個對象被引用了多少次;

2.當新創建一個對象時,它的refcount 屬性被設置爲1 ;

3.當對一個對象進行共享時,Redis 將這個對象的refcount 增一;

4.當使用完一個對象之後,或者取消對共享對象的引用之後,程序將對象的refcount 減一;

5.當對象的refcount 降至0 時,這個redisObject 結構,以及它所引用的數據結構的內存,都會被釋放

3.使用何種編碼:

A.對於String類型:

int:8個字節的長整型

embstr:小於等於39個字節的字符串

raw:大於39個字節的字符串

embstr與raw的區別:embstr在創建String對象時,會分配一次空間,包含redisObject與sds,而raw會分配2次空間,分別分配給redisoObject與sds

B.對於hash類型:

當hash類型元素小於hash-max-ziplist-entries配置(默認512個)、且所有值都小於hash-max-ziplist-value配置(默認64字節)時,Redis會使用ziplist作爲哈希的內部實現;

當hash類型無法滿足ziplist條件時,會使用hashtable編碼

C.對於list類型:

當列表的元素個數小於list-max-ziplist-entries配置(默認512個),且列表中每個元素的值都小於list-max-ziplist-value配置時(默認64字節),Redis會選用ziplist來作爲列表的內部實現;

當列表類型無法滿足ziplist條件時會使用linkedlist作爲內部實現

D.對於set類型:

當集合中的元素都是整數且元素個數小於set-maxintset-entries配置(默認512個)時,Redis會選用intset來作爲集合的內部實現;

當集合類型無法滿足intset的條件時,Redis會使用hashtable作爲集合的內部實現

F.對於zset類型:

當有序集合的元素個數小於zset-max-ziplistentries配置(默認128個)且每個元素的值都小於zset-max-ziplist-value配置(默認64字節)時,Redis會用ziplist來作爲有序集合的內部實現;

當ziplist條件不滿足時,有序集合會使用skiplist作爲內部實現

可使用object encoding key 查看指定key當前的編碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章