Go最新Redis源码分析之双索引机制_redis zzlinsert(2),2024年最新做了3年Golang还没看过OkHttp源码

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) {
zskiplistNode *x;
unsigned long traversed = 0;
int i;
// 获取跳表的表头
x = zsl->header;
// 从最大层数开始逐一遍历
for (i = zsl->level-1; i >= 0; i–) {
while (x->level[i].forward && (traversed + x->level[i].span) <= rank)
{
traversed += x->level[i].span;
x = x->level[i].forward;
}
if (traversed == rank) {
return x;
}
}
return NULL;
}


这种设计使得查找效率提升了,但也会有一定的负面影响,那就是为了维持相邻两层上的结点数的比例为2:1,一旦新增结点或删除结点就需要调整数据结构,从而带来额外的开销。为了避免这种问题,跳表在创建结点时,采用的是另外一种设计方法——随机生成每个结点的层数。此时,相邻的两层链表上的结点数不需要严格的是2:1的关系,降低了插入操作的复杂度。


看一下插入操作的代码,如下所示,



/* Insert a new node in the skiplist. Assumes the element does not already

  • exist (up to the caller to enforce that). The skiplist takes ownership

  • of the passed SDS string ‘ele’. */
    zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i–) {
    /* store rank that is crossed to reach the insert position /
    rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
    while (x->level[i].forward &&
    (x->level[i].forward->score < score ||
    (x->level[i].forward->score == score &&
    sdscmp(x->level[i].forward->ele,ele) < 0)))
    {
    rank[i] += x->level[i].span;
    x = x->level[i].forward;
    }
    update[i] = x;
    }
    /
    we assume the element is not already inside, since we allow duplicated

    • scores, reinserting the same element should never happen since the

    • caller of zslInsert() should test in the hash table if the element is

    • already inside or not. */
      level = zslRandomLevel();
      if (level > zsl->level) {
      for (i = zsl->level; i < level; i++) {
      rank[i] = 0;
      update[i] = zsl->header;
      update[i]->level[i].span = zsl->length;
      }
      zsl->level = level;
      }
      x = zslCreateNode(level,score,ele);
      for (i = 0; i < level; i++) {
      x->level[i].forward = update[i]->level[i].forward;
      update[i]->level[i].forward = x;

      /* update span covered by update[i] as x is inserted here */
      x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
      update[i]->level[i].span = (rank[0] - rank[i]) + 1;
      }

    /* increment span for untouched levels */
    for (i = level; i < zsl->level; i++) {
    update[i]->level[i].span++;
    }

    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
    x->level[0].forward->backward = x;
    else
    zsl->tail = x;
    zsl->length++;
    return x;
    }


zslRandomLevel函数决定了跳表结点层数。层数初始化为1,然后生成随机数小于ZSKPLIST\_P(随机数概率阈值)则增加1层,最大层数ZSKIPLIST\_MAXLEVEL为64。代码如下,



#define ZSKIPLIST_MAXLEVEL 64 /* Should be enough for 2^64 elements /
#define ZSKIPLIST_P 0.25 /
Skiplist P = 1/4 /
/
Returns a random level for the new skiplist node we are going to create.

  • The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
  • (both inclusive), with a powerlaw-alike distribution where higher
  • levels are less likely to be returned. */
    int zslRandomLevel(void) {
    // 初始化层数
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
    level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
    }

### 哈希表和跳表组合


哈希表的数据结构就不多说了,那么这两种索引结构如何组合使用的?


在创建一个zset时,代码会先调用dictCreate函数创建哈希表,再调用zslCreate函数创建跳表。如下所示,



    zs = zmalloc(sizeof(*zs));
    zs->dict = dictCreate(&zsetDictType,NULL);
    zs->zsl = zslCreate();

在Sorted Set插入数据时会调用zsetAdd函数,下面看一下该函数,



/* Add a new element or update the score of an existing element in a sorted

  • set, regardless of its encoding.

  • The set of flags change the command behavior. They are passed with an integer

  • pointer since the function will clear the flags and populate them with

  • other flags to indicate different conditions.

  • The input flags are the following:

  • ZADD_INCR: Increment the current element score by ‘score’ instead of updating

  •        the current element score. If the element does not exist, we
    
  •        assume 0 as previous score.
    
  • ZADD_NX: Perform the operation only if the element does not exist.

  • ZADD_XX: Perform the operation only if the element already exist.

  • When ZADD_INCR is used, the new score of the element is stored in

  • ‘*newscore’ if ‘newscore’ is not NULL.

  • The returned flags are the following:

  • ZADD_NAN: The resulting score is not a number.

  • ZADD_ADDED: The element was added (not present before the call).

  • ZADD_UPDATED: The element score was updated.

  • ZADD_NOP: No operation was performed because of NX or XX.

  • Return value:

  • The function returns 1 on success, and sets the appropriate flags

  • ADDED or UPDATED to signal what happened during the operation (note that

  • none could be set if we re-added an element using the same score it used

  • to have, or in the case a zero increment is used).

  • The function returns 0 on erorr, currently only when the increment

  • produces a NAN condition, or when the ‘score’ value is NAN since the

  • start.

  • The commad as a side effect of adding a new element may convert the sorted

  • set internal encoding from ziplist to hashtable+skiplist.

  • Memory managemnet of ‘ele’:

  • The function does not take ownership of the ‘ele’ SDS string, but copies

  • it if needed. */
    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. */
    if (isnan(score)) {
    *flags = ZADD_NAN;
    return 0;
    }

    /* Update the sorted set according to its encoding. */
    // 如果采用ziplist编码方式,zsetAdd函数的处理逻辑
    if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
    unsigned char *eptr;

     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. */
         if (score != curscore) {
             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. */
         zobj->ptr = zzlInsert(zobj->ptr,ele,score);
         if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries ||
             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;
     }
    

    // 如果采用zipList的编码方式,zsetAdd函数的处理逻辑
    } 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;
     }
    

    } else {
    serverPanic(“Unknown sorted set encoding”);
    }
    return 0; /* Never reached. */
    }


zsetAdd函数会先判断zset时采用ziplist的编码方式还是skiplist的编码方式。如果是skiplist的编码方式,它会先调用哈希表的dictFind函数,查找要插入的元素是否存在。如果插入的元素不存在,则直接调用zslInsert和dictAdd插入新元素;如果插入的元素存在,则zsetAdd会判断是否需要增加元素的权重值。如果权重值发生了变化,zsetAdd函数会调用zslUpdateScore函数,更新跳表中的元素权重值。紧接着,zsetAdd函数会把哈希表中该元素指向跳表结点中的权重值,这样一来,哈希表中元素的权重值就可以保持最新值。


## 总结


Sorted Set数据类型的底层实现同时采用了哈希表和跳表两种结构设计,提高了检索效率。哈希表能够快速的查找单个元素及其权重值,而跳表能快速快速检索到某一个结点是否在跳表的数据结构中。


那么编码方式是如何进行选择的呢?针对不同长度的数据,使用不同大小的元数据信息(prevlen和encoding),这样可以有效的节省内存开销。当有序集合元素数量小于128且所有元素长度小于64byte,zset采用的是ziplist编码方式。


可以看一下ziplist.c源码中的zipStoreEntryEncoding函数,



/* Write the encoidng header of the entry in ‘p’. If p is NULL it just returns

  • the amount of bytes required to encode such a length. Arguments:

  • ‘encoding’ is the encoding we are using for the entry. It could be

  • ZIP_INT_* or ZIP_STR_* or between ZIP_INT_IMM_MIN and ZIP_INT_IMM_MAX

  • for single-byte small immediate integers.

  • ‘rawlen’ is only used for ZIP_STR_* encodings and is the length of the

  • srting that this entry represents.

  • The function returns the number of bytes used by the encoding/length

  • header stored in ‘p’. */
    unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
    unsigned char len = 1, buf[5];

    if (ZIP_IS_STR(encoding)) {
    /* Although encoding is given it may not be set for strings,
    * so we determine it here using the raw length. */
    if (rawlen <= 0x3f) {
    if (!p) return len;
    buf[0] = ZIP_STR_06B | rawlen;
    } else if (rawlen <= 0x3fff) {
    len += 1;
    if (!p) return len;
    buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
    buf[1] = rawlen & 0xff;

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

rawlen & 0xff;

[外链图片转存中…(img-t4BUEJc3-1715886534774)]
[外链图片转存中…(img-SDS7LqOc-1715886534774)]
[外链图片转存中…(img-VrN91TFC-1715886534774)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • 16
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值