redis源碼分析之對象系統源碼分析-string, list鏈表,hash哈希,set集合,zset有序集合

=====================================================
redis源碼學習系列文章:

redis源碼分析之sha1算法分析
redis源碼分析之字典源碼分析
redis源碼分析之內存編碼分析intset, ziplist編碼分析
redis源碼分析之跳躍表
redis源碼分析之內存淘汰策略的原理分析
redis源碼分析之對象系統源碼分析string, list鏈表,hash哈希,set集合,zset有序集合

=====================================================

在我的github上會持續更新Redis代碼的中文分析,地址送出https://github.com/chensongpoixs/credis_source,共同學習進步

前言

在redis中使用八種數據結構都封裝成對象系統

分析流程

  1. redis對象數據結構介紹和對應數據使用編碼格式
  2. string 介紹
  3. list鏈表底層實現原理
  4. hash哈希底層實現原理
  5. set集合底層實現原理
  6. zset有序集合底層實現原理

正文

一, redis對象數據結構介紹和對應數據使用編碼格式

redis中對象的數據結構

typedef struct redisObject {
    unsigned type:4;		// 數據類型的對象
    unsigned encoding:4;	//數據編碼壓縮的格式
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;       // 類似於java中的引用計數 --> shared
    void *ptr;      // 保存redis中的五種數據結構的指針
} robj;
  1. type 就是我們使用命令那些數據結構string, list, hash, set, zset
  2. encoding 是我們使用數據結構底層實現編碼格式
  3. lru 這個涉及到redis的內存淘汰機制
  4. refcount 這個是引用計數,
  5. ptr 這是我們要保持數據的對象指針

1. type在redis中的對象類型

redis中的對象 數據類型
OBJ_STRING 字符串
OBJ_LIST 鏈表
OBJ_SET 集合
OBJ_ZSET 有序集合
OBJ_HASH 哈希

2. 對象底層encoding編碼分析

redis數據類型 編碼格式
OBJ_STRING OBJ_ENCODING_INT long類型編碼格式
OBJ_STRING OBJ_ENCODING_EMBSTR 字符串小於44使用該編碼格式
OBJ_STRING OBJ_ENCODING_RAW 字符串大於44的使用該動態申請內存(sds)
OBJ_LIST OBJ_ENCODING_QUICKLIST 在內存中編碼格式quick_list數據結構
OBJ_LIST OBJ_ENCODING_ZIPLIST 在保存落地文件的時候是以壓縮編碼ziplist格式保存文件中去的,在redis啓動時候要報落地文件中list結構轉換quick_list編碼格式
OBJ_HASH OBJ_ENCODING_ZIPLIST 字符或者數字的長度小64時是要ziplist壓縮編碼
OBJ_HASH OBJ_ENCODING_HT 字符或者數字的長度大於64時使用hashtable編碼
OBJ_SET OBJ_ENCODING_HT hashtable編碼
OBJ_SET OBJ_ENCODING_INTSET intset編碼每個要插入字符都要檢查, 字符過長就是要hashtable編碼格式
OBJ_ZSET OBJ_ENCODING_ZIPLIST 有序集合子字符串小於64字節時使用ziplist編碼格式,在zset中年使用ziplist是兩個節點爲一組數據即key-value
OBJ_ZSET OBJ_ENCODING_SKIPLIST key是哈希表的插入的數字是使用跳躍表的進行排序的,跳躍表的

二, string 底層實現原理

string類型底層編碼有三種格式分別是

  1. long類型
  2. embstr
  3. raw

需要注意的就是redis都命令操作append是基於raw的操作字符串操作

數據都是保持ptr指針這裏的

string結構在內存結構圖

在這裏插入圖片描述

三, list鏈表底層實現原理

list鏈表底層實現有兩種數據的結構分別是

  1. quicklist (快速列表)
  2. ziplist (壓縮編碼列表)

list鏈表是應向有重複數據的這是和它底層實現有關

在redis5.0中內存中使用都是quicklist數據結構實現的,而異步存儲是使用ziplist存儲到落地文件的,讀取落地文件讀取ziplist後要轉換quicklist數據結構

在創建沒有任何適配ziplist數據結構

void pushGenericCommand(client *c, int where) {
    int j, pushed = 0;
    robj *lobj = lookupKeyWrite(c->db,c->argv[1]);

    if (lobj && lobj->type != OBJ_LIST) {
        addReply(c,shared.wrongtypeerr);
        return;
    }

    for (j = 2; j < c->argc; j++) {
		// 第一次時要插入redis的dict中的key即hash值之後都是使用quicktlist連接數據的
        if (!lobj) {
			// 把鏈表的數據結構
            lobj = createQuicklistObject();
			// 設置每個鏈表的可以存儲數據的個數  只是在redis.conf的配置文件中配置的 默認配置的8kb的大小
            quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
                                server.list_compress_depth);
			// 把hash值插入redis的dict中去
            dbAdd(c->db, c->argv[1], lobj);
        }
		// 插入鏈表中的使用
        listTypePush(lobj,c->argv[j],where);
        pushed++;
    }
    addReplyLongLong(c, (lobj ? listTypeLength(lobj) : 0));
    if (pushed) {
        char *event = (where == LIST_HEAD) ? "lpush" : "rpush";

        signalModifiedKey(c->db,c->argv[1]);
        notifyKeyspaceEvent(NOTIFY_LIST,event,c->argv[1],c->db->id);
    }
    server.dirty += pushed;
}

讀取文件

//讀取落地文件時是ziplist轉換爲quicklist的數據結構
  } else if (rdbtype == RDB_TYPE_LIST) {
        /* Read list value */
        if ((len = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL;

        o = createQuicklistObject();
        quicklistSetOptions(o->ptr, server.list_max_ziplist_size,
                            server.list_compress_depth);

        /* Load every single element of the list */
        while(len--) {
            if ((ele = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
            dec = getDecodedObject(ele);
            size_t len = sdslen(dec->ptr);
            quicklistPushTail(o->ptr, dec->ptr, len);
            decrRefCount(dec);
            decrRefCount(ele);
        }

存儲文件

} else if (o->type == OBJ_LIST) {
        /* Save a list value */
		// list保存數據的是轉換ziplist保存到落地文件
        if (o->encoding == OBJ_ENCODING_QUICKLIST) {
            quicklist *ql = o->ptr;
            quicklistNode *node = ql->head;

            if ((n = rdbSaveLen(rdb,ql->len)) == -1) return -1;
            nwritten += n;

            while(node) {
                if (quicklistNodeIsCompressed(node)) {
                    void *data;
                    size_t compress_len = quicklistGetLzf(node, &data);
                    if ((n = rdbSaveLzfBlob(rdb,data,compress_len,node->sz)) == -1) return -1;
                    nwritten += n;
                } else {
                    if ((n = rdbSaveRawString(rdb,node->zl,node->sz)) == -1) return -1;
                    nwritten += n;
                }
                node = node->next;
            }
        } else {
            serverPanic("Unknown list encoding");
        }

在redis內存佈局

在這裏插入圖片描述

四, hash哈希底層實現原理

哈希表的底層實現有兩種分別是

  1. ziplist壓縮編碼
  2. hashtable

在哈希表中每次插入數據或者修改都會檢查當編碼是ziplist時數據的長度是否大於配置表的hash-max-ziplist-value大於就使用hashtable表數據結構對象

在使用ziplist存儲key-value時都是使用兩個節點存儲的,在查找和刪除時都兩個節點一迭代的key-value兩個節點是想連接在一起的

int hashTypeSet(robj *o, sds field, sds value, int flags) {
    int update = 0;
  if (o->encoding == OBJ_ENCODING_ZIPLIST) {
        unsigned char *zl, *fptr, *vptr;

        zl = o->ptr;
        fptr = ziplistIndex(zl, ZIPLIST_HEAD);
        if (fptr != NULL) {
            fptr = ziplistFind(fptr, (unsigned char*)field, sdslen(field), 1);
            if (fptr != NULL) {
                /* Grab pointer to the value (fptr points to the field) */
                vptr = ziplistNext(zl, fptr);
                serverAssert(vptr != NULL);
                update = 1;

                /* Delete value */
                zl = ziplistDelete(zl, &vptr);

                /* Insert new value */
                zl = ziplistInsert(zl, vptr, (unsigned char*)value,
                        sdslen(value));
            }
        }

        if (!update) {
            /* Push new field/value pair onto the tail of the ziplist */
            zl = ziplistPush(zl, (unsigned char*)field, sdslen(field),
                    ZIPLIST_TAIL);
            zl = ziplistPush(zl, (unsigned char*)value, sdslen(value),
                    ZIPLIST_TAIL);
        }
        o->ptr = zl;

        /* Check if the ziplist needs to be converted to a hash table */
        if (hashTypeLength(o) > server.hash_max_ziplist_entries)
            hashTypeConvert(o, OBJ_ENCODING_HT);

1, ziplist 在內存佈局

在這裏插入圖片描述

2,hashtable

中哈希碰撞時沒有處理只是增加鏈表

哈希表沒有什麼好說的

五, set集合底層實現原理

set集合底層編碼有兩種分別是

  1. intset整數編碼
  2. hashtable

整數
當前編碼是intset每次都會檢查要插入的數據是否可以轉換longlong 不可以就轉換編碼格式hastble,還有插入後檢查當前intset這一塊數據是否大於配置表中set-max-intset-entries的值, 大於就要轉換hashtable的編碼格式

 } else if (subject->encoding == OBJ_ENCODING_INTSET) {
		// 集合如果intset編碼格式會對每一個要插入數據進行檢查是否是轉換longlong, 不可以就轉換hashtable表的編碼格式
        if (isSdsRepresentableAsLongLong(value,&llval) == C_OK) {
            uint8_t success = 0;
			// 這個插入的有一個點講究哦, 可能要轉換編碼格式哦
			// intset中的整數編碼四種格式
			// 1. 一個字節
			// 2. 二個字節
			// 3. 四個字節
			// 4. 八個字節
            subject->ptr = intsetAdd(subject->ptr,llval,&success);
            if (success) {
                /* Convert to regular set when the intset contains
                 * too many entries. */
				// intset 整數編碼格式的長度是否大於配置表的中的大小如果大於就要的修改成hashtable的編碼格式了
                if (intsetLen(subject->ptr) > server.set_max_intset_entries)
                    setTypeConvert(subject,OBJ_ENCODING_HT);
                return 1;
            }
        } else {
            /* Failed to get integer from object, convert to regular set. */
            setTypeConvert(subject,OBJ_ENCODING_HT);

            /* The set *was* an intset and this value is not integer
             * encodable, so dictAdd should always work. */
            serverAssert(dictAdd(subject->ptr,sdsdup(value),NULL) == DICT_OK);
            return 1;
        }

在set集合中不應許有重複數據是因爲intset在插入的時候就使用二分查找法定位數據的下標了

static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
    int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
    int64_t cur = -1;

    /* The value can never be found when the set is empty */
    if (intrev32ifbe(is->length) == 0) {
        if (pos) *pos = 0;
        return 0;
    } else {
        /* Check for the case where we know we cannot find the value,
         * but do know the insert position. */
		// 檢查數據中最後一個和前一個和要插入的數據比較是否得到相對位置的下標的 -[相對位置的下標是0位置是否大於0或者小0的比較]
        if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
            if (pos) *pos = intrev32ifbe(is->length);
            return 0;
        } else if (value < _intsetGet(is,0)) {
            if (pos) *pos = 0;
            return 0;
        }
    }

    while(max >= min) {
		// 二叉查找法 -> 中位置
        mid = ((unsigned int)min + (unsigned int)max) >> 1;
        cur = _intsetGet(is,mid);
        if (value > cur) {
            min = mid+1;
        } else if (value < cur) {
            max = mid-1;
        } else {
            break;
        }
    }
	// 如果存在就不會在插入數據
    if (value == cur) {
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
}

intset在內存佈局

在這裏插入圖片描述

hashtable中的value插入一個空值就可以了

六, zset有序集合底層實現原理

zset有序集合編碼有兩種分別是

  1. ziplist壓縮編碼
  2. skiplist跳躍表

key的值大於64字節就使用skiplist跳躍表編碼格式

ziplist編碼使用和hash種使用一樣的但是有一個區別是在hash中ziplist插入key-value是沒有順序的在zset中是有序的所以zset在插入時想表ziplist中的score的值找到要插入的位置,修改的時候先是刪除在重新查找要插入的位置

int zsetAdd(robj *zobj, double score, sds ele, int *flags, double *newscore) {
    /* Turn options into simple to check vars. */
    int incr = (*flags & ZADD_INCR) != 0;
    int nx = (*flags & ZADD_NX) != 0;
    int xx = (*flags & ZADD_XX) != 0;
    *flags = 0; /* We'll return our response flags. */
    double curscore;

    /* NaN as input is an error regardless of all the other parameters. */
	// 是否score數據是否符合要求
    if (isnan(score)) {
        *flags = ZADD_NAN;
        return 0;
    }

    /* Update the sorted set according to its encoding. */
    if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
        unsigned char *eptr;
		// 查找key 這裏裏面已經修改 迭代器的指針兩個節點一迭代
        if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) {
            /* NX? Return, same element already exists. */
            if (nx) {
                *flags |= ZADD_NOP;
                return 1;
            }

            /* Prepare the score for the increment if needed. */
            if (incr) {
                score += curscore;
                if (isnan(score)) {
                    *flags |= ZADD_NAN;
                    return 0;
                }
                if (newscore) *newscore = score;
            }

            /* Remove and re-insert when score changed. */
			// score是否相同不相同就刪除添加進入
			// 1. 更新操作
            if (score != curscore) {
				// 刪除兩個節點數據的在zset有序集合中自己封裝的刪除節點
                zobj->ptr = zzlDelete(zobj->ptr, eptr);
                zobj->ptr = zzlInsert(zobj->ptr, ele, score);
                *flags |= ZADD_UPDATED;
            }
            return 1;
        } else if (!xx) {
            /* Optimize: check if the element is too large or the list
             * becomes too long *before* executing zzlInsert. */
            // 2. 直接插入
			zobj->ptr = zzlInsert(zobj->ptr,ele,score);
            if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries)
                zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
            if (sdslen(ele) > server.zset_max_ziplist_value)
                zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
            if (newscore) *newscore = score;
            *flags |= ZADD_ADDED;
            return 1;
        } else {
            *flags |= ZADD_NOP;
            return 1;
        }

使用skiplist跳躍表和hashtable一起使用的查找比較快的下面的我就不說了



} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
        zset *zs = zobj->ptr;
        zskiplistNode *znode;
        dictEntry *de;

        de = dictFind(zs->dict,ele);
        if (de != NULL) {
            /* NX? Return, same element already exists. */
            if (nx) {
                *flags |= ZADD_NOP;
                return 1;
            }
            curscore = *(double*)dictGetVal(de);

            /* Prepare the score for the increment if needed. */
            if (incr) {
                score += curscore;
                if (isnan(score)) {
                    *flags |= ZADD_NAN;
                    return 0;
                }
                if (newscore) *newscore = score;
            }

            /* Remove and re-insert when score changes. */
            if (score != curscore) {
                znode = zslUpdateScore(zs->zsl,curscore,ele,score);
                /* Note that we did not removed the original element from
                 * the hash table representing the sorted set, so we just
                 * update the score. */
                dictGetVal(de) = &znode->score; /* Update score ptr. */
                *flags |= ZADD_UPDATED;
            }
            return 1;
        } else if (!xx) {
            ele = sdsdup(ele);
            znode = zslInsert(zs->zsl,score,ele);
            serverAssert(dictAdd(zs->dict,ele,&znode->score) == DICT_OK);
            *flags |= ZADD_ADDED;
            if (newscore) *newscore = score;
            return 1;
        } else {
            *flags |= ZADD_NOP;
            return 1;
        }
		
		
//----------------------------------------------




zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i;

    /* We need to seek to element to update to start: this is useful anyway,
     * we'll have to update or remove it. */
    x = zsl->header;
	// 找到跳躍表的節點指針  跳躍表中分等級的哦每個等級中的數據的個數可能相等哦
    for (i = zsl->level-1; i >= 0; i--) {
		// 遍歷每個等級中的數據的於當前節點的數據的是要修改的節點數據的
        while (x->level[i].forward &&
                (x->level[i].forward->score < curscore ||
                    (x->level[i].forward->score == curscore &&
                     sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            x = x->level[i].forward;
        }
        update[i] = x;
    }

    /* Jump to our element: note that this function assumes that the
     * element with the matching score exists. */
    x = x->level[0].forward;
    serverAssert(x && curscore == x->score && sdscmp(x->ele,ele) == 0);

    /* If the node, after the score update, would be still exactly
     * at the same position, we can just update the score without
     * actually removing and re-inserting the element in the skiplist. */
	// 判斷節點判斷保持數據的是否需要的修改位置 跳躍表中的數據的是從小到大的排序的
    if ((x->backward == NULL || x->backward->score < newscore) &&
        (x->level[0].forward == NULL || x->level[0].forward->score > newscore))
    {
        x->score = newscore;
        return x;
    }

    /* No way to reuse the old node: we need to remove and insert a new
     * one at a different place. */
	// 說明要修改的數據的score大於當前的節點的score的值的所以需要的刪除了當前節點從新插入的節點數據的
    zslDeleteNode(zsl, x, update);
	// 插入的跳躍表的節點
    zskiplistNode *newnode = zslInsert(zsl,newscore,x->ele);
    /* We reused the old node x->ele SDS string, free the node now
     * since zslInsert created a new one. */
    x->ele = NULL;
    zslFreeNode(x);
    return newnode;
}

結語

個人博客地址

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