redis基本數(shù)據(jù)結(jié)構(gòu)

redis 基本數(shù)據(jù)結(jié)構(gòu).


redis的基本數(shù)據(jù)結(jié)構(gòu)主要有: SDS動態(tài)字符串,鏈表,字典,哈希表,跳躍表,整數(shù)集合,壓縮列表.

SDS 動態(tài)字符串

typedef char *sds;

struct sdshdr {
//記錄已使用的長度
    int len;
//記錄buf中未使用字符串
    int free;
//字節(jié)數(shù)據(jù),保存字符串
    char buf[];
};

分配內(nèi)存時,會多分配一個字節(jié)來存放'\0'結(jié)束標(biāo)記.

注意: 在sdshdr結(jié)構(gòu)體中,buf未分配內(nèi)存時,buf是不占空間的

int main(){

    printf("int --- %ld\n",sizeof(int));
    printf("struct sdshdr --- %ld\n", sizeof(struct sdshdr));
    return 0;
}
輸出: 
int --- 4
struct sdshdr --- 8
//分配空間
sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;

    if (init) {
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }
    if (sh == NULL) return NULL;
    sh->len = initlen;
    sh->free = 0;
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    sh->buf[initlen] = '\0';
    return (char*)sh->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;
}

優(yōu)點:

  • 常數(shù)復(fù)雜度獲取字符串的長度
  • 杜絕緩沖區(qū)溢出
  • 減少修改字符串時重新分配內(nèi)存的次數(shù)
    1. 空間預(yù)分配
    2. 惰性回收
  • 二進制安全
  • 兼容部分C字符串


鏈表

/*
 * 雙端鏈表節(jié)點
 */
typedef struct listNode {

    // 前置節(jié)點
    struct listNode *prev;

    // 后置節(jié)點
    struct listNode *next;

    // 節(jié)點的值
    void *value;

} listNode;

/*
 * 雙端鏈表迭代器
 */
typedef struct listIter {

    // 當(dāng)前迭代到的節(jié)點
    listNode *next;

    // 迭代的方向
    int direction;

} listIter;

/*
 * 雙端鏈表結(jié)構(gòu)
 */
typedef struct list {

    // 表頭節(jié)點
    listNode *head;

    // 表尾節(jié)點
    listNode *tail;

    // 節(jié)點值復(fù)制函數(shù)
    void *(*dup)(void *ptr);

    // 節(jié)點值釋放函數(shù)
    void (*free)(void *ptr);

    // 節(jié)點值對比函數(shù)
    int (*match)(void *ptr, void *key);

    // 鏈表所包含的節(jié)點數(shù)量
    unsigned long len;

} list;

字典

Key-Value

/*
 * 哈希表節(jié)點
 */
typedef struct dictEntry {
    
    // 鍵
    void *key;

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

    // 指向下個哈希表節(jié)點,形成鏈表
    struct dictEntry *next;

} dictEntry;

哈希表

/*
 * 哈希表
 *
 * 每個字典都使用兩個哈希表,從而實現(xiàn)漸進式 rehash 。
 */
typedef struct dictht {
    
    // 哈希表數(shù)組
    dictEntry **table;

    // 哈希表大小
    unsigned long size;
    
    // 哈希表大小掩碼,用于計算索引值
    // 總是等于 size - 1
    unsigned long sizemask;

    // 該哈希表已有節(jié)點的數(shù)量
    unsigned long used;

} dictht;

字典


/*
 * 字典
 */
typedef struct dict {

    // 類型特定函數(shù)
    dictType *type;

    // 私有數(shù)據(jù)
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 當(dāng) rehash 不在進行時,值為 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在運行的安全迭代器的數(shù)量
    int iterators; /* number of iterators currently running */

} dict;

字典的普通狀態(tài):


  • 索引值計算
    h = dictHashKey(ht, key) & ht->sizemask;
  • 解決鍵沖突
    當(dāng)有兩個或以上數(shù)量的鍵被分配到哈希表上的同一索引的時候,我們稱這些鍵發(fā)生了沖突,redis通過鏈地址法來解決沖突
  • rehash
    當(dāng)哈希表保存的鍵值對數(shù)量太多或者太少時,要進行rehash
    漸進式rehash: http://www.itdecent.cn/p/9c84856cd5c0

跳躍表

跳躍表節(jié)點

/*
 * 跳躍表節(jié)點
 */
typedef struct zskiplistNode {

    // 成員對象
    robj *obj;

    // 分值
    double score;

    // 后退指針
    struct zskiplistNode *backward;

    // 層
    struct zskiplistLevel {

        // 前進指針
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;

跳躍表


/*
 * 跳躍表
 */
typedef struct zskiplist {

    // 表頭節(jié)點和表尾節(jié)點
    struct zskiplistNode *header, *tail;

    // 表中節(jié)點的數(shù)量
    unsigned long length;

    // 表中層數(shù)最大的節(jié)點的層數(shù)
    int level;

} zskiplist;
//隨機獲取層數(shù)
int zslRandomLevel(void) {
    int level = 1;

    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;

    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

整數(shù)集合

整數(shù)集合是集合鍵的底層實現(xiàn)之一,當(dāng)一個集合只包含整數(shù)值,并且這個集合元素不多時,redis會使用整數(shù)集合作為集合鍵的底層實現(xiàn).

typedef struct intset {
    
    // 編碼方式
    uint32_t encoding;

    // 集合包含的元素數(shù)量
    uint32_t length;

    // 保存元素的數(shù)組
    int8_t contents[];

} intset;

encoding: 表示整數(shù)集合中,存放的是那種長度的整數(shù):16位,32位,64位.


  • 升級
    當(dāng)添加一個整數(shù)到整數(shù)集合中時,如果新元素的長度比集合中的編碼方式都要長時,要發(fā)生升級動作 , 整數(shù)集合只支持升級操作,不支持降級操作.

/* Insert an integer in the intset 
 * 
 * 嘗試將元素 value 添加到整數(shù)集合中。
 *
 * *success 的值指示添加是否成功:
 * - 如果添加成功,那么將 *success 的值設(shè)為 1 。
 * - 因為元素已存在而造成添加失敗時,將 *success 的值設(shè)為 0 。
 *
 * T = O(N)
 */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {

    // 計算編碼 value 所需的長度
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;

    // 默認(rèn)設(shè)置插入為成功
    if (success) *success = 1;

    /* Upgrade encoding if necessary. If we need to upgrade, we know that
     * this value should be either appended (if > 0) or prepended (if < 0),
     * because it lies outside the range of existing values. */
    // 如果 value 的編碼比整數(shù)集合現(xiàn)在的編碼要大
    // 那么表示 value 必然可以添加到整數(shù)集合中
    // 并且整數(shù)集合需要對自身進行升級,才能滿足 value 所需的編碼
    if (valenc > intrev32ifbe(is->encoding)) {
        /* This always succeeds, so we don't need to curry *success. */
        // T = O(N)
        return intsetUpgradeAndAdd(is,value);
    } else {
        // 運行到這里,表示整數(shù)集合現(xiàn)有的編碼方式適用于 value

        /* Abort if the value is already present in the set.
         * This call will populate "pos" with the right position to insert
         * the value when it cannot be found. */
        // 在整數(shù)集合中查找 value ,看他是否存在:
        // - 如果存在,那么將 *success 設(shè)置為 0 ,并返回未經(jīng)改動的整數(shù)集合
        // - 如果不存在,那么可以插入 value 的位置將被保存到 pos 指針中
        //   等待后續(xù)程序使用
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }

        // 運行到這里,表示 value 不存在于集合中
        // 程序需要將 value 添加到整數(shù)集合中
    
        // 為 value 在集合中分配空間
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        // 如果新元素不是被添加到底層數(shù)組的末尾
        // 那么需要對現(xiàn)有元素的數(shù)據(jù)進行移動,空出 pos 上的位置,用于設(shè)置新值
        // 舉個例子
        // 如果數(shù)組為:
        // | x | y | z | ? |
        //     |<----->|
        // 而新元素 n 的 pos 為 1 ,那么數(shù)組將移動 y 和 z 兩個元素
        // | x | y | y | z |
        //         |<----->|
        // 這樣就可以將新元素設(shè)置到 pos 上了:
        // | x | n | y | z |
        // T = O(N)
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }

    // 將新值設(shè)置到底層數(shù)組的指定位置中
    _intsetSet(is,pos,value);

    // 增一集合元素數(shù)量的計數(shù)器
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);

    // 返回添加新元素后的整數(shù)集合
    return is;
}

升級操作:

/* Upgrades the intset to a larger encoding and inserts the given integer. 
 *
 * 根據(jù)值 value 所使用的編碼方式,對整數(shù)集合的編碼進行升級,
 * 并將值 value 添加到升級后的整數(shù)集合中。
 *
 * 返回值:添加新元素之后的整數(shù)集合
 *
 * T = O(N)
 */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    
    // 當(dāng)前的編碼方式
    uint8_t curenc = intrev32ifbe(is->encoding);

    // 新值所需的編碼方式
    uint8_t newenc = _intsetValueEncoding(value);

    // 當(dāng)前集合的元素數(shù)量
    int length = intrev32ifbe(is->length);

    // 根據(jù) value 的值,決定是將它添加到底層數(shù)組的最前端還是最后端
    // 注意,因為 value 的編碼比集合原有的其他元素的編碼都要大
    // 所以 value 要么大于集合中的所有元素,要么小于集合中的所有元素
    // 因此,value 只能添加到底層數(shù)組的最前端或最后端
    int prepend = value < 0 ? 1 : 0;

    /* First set new encoding and resize */
    // 更新集合的編碼方式
    is->encoding = intrev32ifbe(newenc);
    // 根據(jù)新編碼對集合(的底層數(shù)組)進行空間調(diào)整
    // T = O(N)
    is = intsetResize(is,intrev32ifbe(is->length)+1);

    /* Upgrade back-to-front so we don't overwrite values.
     * Note that the "prepend" variable is used to make sure we have an empty
     * space at either the beginning or the end of the intset. */
    // 根據(jù)集合原來的編碼方式,從底層數(shù)組中取出集合元素
    // 然后再將元素以新編碼的方式添加到集合中
    // 當(dāng)完成了這個步驟之后,集合中所有原有的元素就完成了從舊編碼到新編碼的轉(zhuǎn)換
    // 因為新分配的空間都放在數(shù)組的后端,所以程序先從后端向前端移動元素
    // 舉個例子,假設(shè)原來有 curenc 編碼的三個元素,它們在數(shù)組中排列如下:
    // | x | y | z | 
    // 當(dāng)程序?qū)?shù)組進行重分配之后,數(shù)組就被擴容了(符號 ? 表示未使用的內(nèi)存):
    // | x | y | z | ? |   ?   |   ?   |
    // 這時程序從數(shù)組后端開始,重新插入元素:
    // | x | y | z | ? |   z   |   ?   |
    // | x | y |   y   |   z   |   ?   |
    // |   x   |   y   |   z   |   ?   |
    // 最后,程序可以將新元素添加到最后 ? 號標(biāo)示的位置中:
    // |   x   |   y   |   z   |  new  |
    // 上面演示的是新元素比原來的所有元素都大的情況,也即是 prepend == 0
    // 當(dāng)新元素比原來的所有元素都小時(prepend == 1),調(diào)整的過程如下:
    // | x | y | z | ? |   ?   |   ?   |
    // | x | y | z | ? |   ?   |   z   |
    // | x | y | z | ? |   y   |   z   |
    // | x | y |   x   |   y   |   z   |
    // 當(dāng)添加新值時,原本的 | x | y | 的數(shù)據(jù)將被新值代替
    // |  new  |   x   |   y   |   z   |
    // T = O(N)
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    /* Set the value at the beginning or the end. */
    // 設(shè)置新值,根據(jù) prepend 的值來決定是添加到數(shù)組頭還是數(shù)組尾
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);

    // 更新整數(shù)集合的元素數(shù)量
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);

    return is;
}

壓縮列表

壓縮列表是列表鍵和哈希鍵的底層實現(xiàn)之一,當(dāng)一個列表鍵只包含少量列表項,并且每個列表項要么是小數(shù)值,要么是長度比較短的字符串,那么redis會用壓縮列表作為列表鍵的底層實現(xiàn).

壓縮列表整體結(jié)構(gòu):




空 ziplist 示例圖:


area        |<---- ziplist header ---->|<-- end -->|

size          4 bytes   4 bytes 2 bytes  1 byte
            +---------+--------+-------+-----------+
component   | zlbytes | zltail | zllen | zlend     |
            |         |        |       |           |
value       |  1011   |  1010  |   0   | 1111 1111 |
            +---------+--------+-------+-----------+
                                       ^
                                       |
                               ZIPLIST_ENTRY_HEAD
                                       &
address                        ZIPLIST_ENTRY_TAIL
                                       &
                               ZIPLIST_ENTRY_END

非空 ziplist 示例圖

area        |<---- ziplist header ---->|<----------- entries ------------->|<-end->|

size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN | zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |   ZIPLIST_ENTRY_END
                                                                  |
                                                        ZIPLIST_ENTRY_TAIL

entry結(jié)構(gòu):

/*
 * 保存 ziplist 節(jié)點信息的結(jié)構(gòu)
 */
typedef struct zlentry {

    // prevrawlen :前置節(jié)點的長度
    // prevrawlensize :編碼 prevrawlen 所需的字節(jié)大小
    unsigned int prevrawlensize, prevrawlen;

    // len :當(dāng)前節(jié)點值的長度
    // lensize :編碼 len 所需的字節(jié)大小
    unsigned int lensize, len;

    // 當(dāng)前節(jié)點 header 的大小
    // 等于 prevrawlensize + lensize
    unsigned int headersize;

    // 當(dāng)前節(jié)點值所使用的編碼類型
    unsigned char encoding;

    // 指向當(dāng)前節(jié)點的指針
    unsigned char *p;

} zlentry;

  • previous_entry_length
    以字節(jié)為單位,記錄了壓縮列表前一個節(jié)點的長度.,該屬性的長度可以是1字節(jié),或者5字節(jié). 如果前節(jié)點的長度小于254字節(jié),則previous_entry_length的長度為1字節(jié),如果前一節(jié)點的長度大于254字節(jié),則previous_entry_length的長度為5字節(jié).其屬性的第一字節(jié)被設(shè)置為0xFE,而后4個字節(jié)保存前一節(jié)點的長度.

  • encoding
    記錄節(jié)點的content屬性保存數(shù)據(jù)的類型及長度
    字節(jié)數(shù)組編碼



    整數(shù)編碼


  • content
    保存節(jié)點的值
    字節(jié)數(shù)組:



    整數(shù)值:


連鎖更新:
有一種情況,在一個壓縮列表中,有多個連續(xù)的,長度在250字節(jié)---253字節(jié)的節(jié)點,他們的previous_entry_length長度都是1字節(jié),當(dāng)我們在頭部插入一個大于254字節(jié)長度的節(jié)點時,第二個節(jié)點要用5個字節(jié)來保存前一個節(jié)點的長度,但是現(xiàn)在只有1個字節(jié),要進行擴容到5字節(jié),而第三個節(jié)點要保存第二個節(jié)點的長度,由于第二個節(jié)點的previous_entry_length屬性由1字節(jié)變?yōu)榱?字節(jié),第二個節(jié)點的整體長度也超過了254,第三個節(jié)點也需要擴容.這樣一直更新下去,就會發(fā)生連鎖更新.
連鎖更新最壞的復(fù)雜度為O(N2);

添加節(jié)點到要列表或者從壓縮列表刪除節(jié)點,都會發(fā)生連鎖更新操作,但是這個幾率并不高.

參考----<redis設(shè)計與實現(xiàn)>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容