Redis之动态字符串 链表 跳跃表 整数集

分析结构体 + 重要API = 理清楚了架构。
Redis是键值,内存缓冲系统。Memcached内部仅仅支持字符串,直接用一个hash表管理。但是Redis支持数据类型更多,因此内部肯定使用了更过的数据结构。

内部数据结构是Redis高效的基础,这里主要详解内部最基本的数据结构以及操作其最基本的API。例如跳跃表,详解其创建、插入、删除、查找API,其他功能封装这些API不做详细解答。

柔性数组说明

柔性数据的好处在于,结构体里面既可以包含空间大小,也可以使用标记直接访问分配的内存,内存空间不足,直接使用realloc(空间重分配、复制原数据、释放原空间、返回新空间首地址)。memcached,Item内部的data域也使用了柔性数组。
陈皓-酷壳柔性数组
维基-柔性数组

动态字符串

1、介绍
C字符串叫做字符串字面量,对于不需要修改的字符串,使用这种默认表示则很简单。那作者为什么还需要造轮子,搞个SDS(Simple Dynamic String)?
封装字符串修改、扩容、缩容、拼接等可能都需要手动重新分配内存的细节,让客户端仅仅专注于上层使用。好处在下面几点。

  • 因为使用了sdshrd结构,内部存储了字符串长度以及使用内存信息,可以快速定位字符串长度O(1),避免了strlen的O(N)的遍历。
  • 扩展字符串长度,需要动态分配内存,全部右sdshrd的API自动完成,避免了手动重新分配内存,然后memcpy的繁琐。实际上sdshrd内部也是按照这个完成。
  • 将一个字符串拼接到一个字符串后面,对于sds绝对不会缓冲区溢出,因为拼接之前会进行内层检测。
  • sds具有内存预分配的作用,避免重复多次进行内存分配工作。
  • 二进制安全,因为sds有字符串length信息,所以不需要以’/0’为结尾区分字符串,使得Redis内部不仅仅可以保存文本信息也可以存储图片,视频等二进制数据信息。但是为了兼容string内部函数功能,Redis的sds还是默认以’/0’结尾。

2、结构体部分
总共有3种结构体,完全是为了节约存储len和alloc的内存而设计。作者绝对是嵌入式出生,这么爱节约内存。可见作者的功力深厚。结构体的选择会依据字符串的长度动态选择。总之就是分配一块连续的内存块管理字符串。

typedef char *sds;//可代表sdshdr首地址,然后通过地址类型转换访问连续内存块
//sds是为了兼容C字符串设计。
/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* 数据区域分配的全部内存,除去了head和'/0'。excluding the header and null terminator */
    unsigned char flags; /* 高3位代表结构体存储len的数据类型,有了整个参数就可以通过buf正确访问len和alloc了。宏定义就是这样做的。3 lsb of type, 5 unused bits */
    char buf[];//柔性数组
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
//强迫症,以上仅仅是为了节约存储len和alloc的内存而设计。

#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));//宏定义连接符##了解下,变量
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))//找出sdshdr16的数据区域
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)

这里写图片描述
直接返回地址buf即可,因为可以通过buf访问前面的len、alloc、flags字节。作者这种玩内存和指针的方法,真是出神入化了。sds[-1]就可以访问flag参数了。地址在堆中总是动态增加。所以可以通过指针的加或者减访问内存块。这就是玩内存的方法了,很聪明哦。

3、创建一个sds字符串
根据传入的C字符串,选择合适的sdshdr__,然后malloc+memcpy即可。整个大体过程就是这样。


/* Create a new sds string starting from a null terminated C string. */
//通过c字符串,创建一个sds。将c字符串复制到sds中,并返回sdshrd中的buf。
sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);//需求长度
    return sdsnewlen(init, initlen);//分配sdshrd,并返回其中的buf数据区域地址。
}

/* Create a new sds string with the content specified by the 'init' pointer
 * and 'initlen'.
 * If NULL is used for 'init' the string is initialized with zero bytes.
 *
 * The string is always null-termined (all the sds strings are, always) so
 * even if you create an sds string with:
 *
 * mystring = sdsnewlen("abc",3);
 *
 * You can print the string with printf() as there is an implicit \0 at the
 * end of the string. However the string is binary safe and can contain
 * \0 characters in the middle, as the length is stored in the sds header. */
sds sdsnewlen(const void *init, size_t initlen) {//创建SDS字符串,直接返回char即可,需要访问SDS,通过指针回溯即可
    void *sh;
    sds s;
    char type = sdsReqType(initlen);//根据输入字符串长度,选择合适的sdshrd,完全为了节约内存。
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);//头结构体节点占用内存。
    unsigned char *fp; /* flags pointer. */

    sh = s_malloc(hdrlen+initlen+1);//分配=头结点+数据长度+结束符
    if (!init)
        memset(sh, 0, hdrlen+initlen+1);//将新分配的sh内存清空。因为malloc并不保证先前位置为空,所以必须设为0
    if (sh == NULL) return NULL;
    s = (char*)sh+hdrlen;//定位head中的数据区域
    fp = ((unsigned char*)s)-1;//sdshdr中的flags域
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);//通过数据域的地址,获取整个sdshrd的地址
            sh->len = initlen;//初始化
            sh->alloc = initlen;//初始化
            *fp = type;//结构体中类型
            break;'/0'
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen);//将init数据全部拷贝到s数据区域
    s[initlen] = '\0';//结束符
    return s;//返回s
}

这里写图片描述
上述过程可以用简单的图表示,很容易理解,所以sds很简单,就不细说了。antirez

4、sdscat将一个字符串加到sds之后,不会内存溢出
整个过程很简单,绝对不会有内存溢出的问题。首先确定sds是否有足够的空间。假如空间足够则直接realloc+memcpy;假如空间不足,则依据总长度选择合适的类型,然后malloc(分配新内存)+free(释放旧内存)+memcpy(先拷贝旧字符串)+memcpy(再拷贝新字符串)即可。如果没有这个函数的封装,利用C字符串,也会是这样的一个过程。


/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
 * end of the specified sds string 's'.
 *
 * After the call, the passed sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call. */
sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);//获取s长度

    s = sdsMakeRoomFor(s,len);//扩容sds,若容量够,则直接拼接,防止溢出
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);//将t直接拷贝到s+curlen位置处。很简单的memcpy。注意内存已分配,绝对不会造成重叠的问题。
    sdssetlen(s, curlen+len);//设置字符串长度
    s[curlen+len] = '\0';//末尾加上空
    return s;//返回s,s可能被修改,也可能没有被修改。
}

/* Enlarge the free space at the end of the sds string so that the caller
 * is sure that after calling this function can overwrite up to addlen
 * bytes after the end of the string, plus one more byte for nul term.
 *
 * Note: this does not change the *length* of the sds string as returned
 * by sdslen(), but only the free buffer space we have. */
sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);//返回sds可用容量
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;//可用容量足够放下addlen长度字符串,直接返回

    /*
        不够则需要进行扩容加上搬移。
    */
    len = sdslen(s);//原来字符串长度
    sh = (char*)s-sdsHdrSize(oldtype);//找到结构体头部
    newlen = (len+addlen);//需要分配新的长度
    if (newlen < SDS_MAX_PREALLOC)//新的长度小于1M,则直接重新分配2倍,为以后使用避免重新分配内存
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;//大于1M,则直接多分配1M

    type = sdsReqType(newlen);//获取相应的sdshrd的类型表示。

    /* Don't use type 5: the user is appending to the string and type 5 is
     * not able to remember empty space, so sdsMakeRoomFor() must be called
     * at every appending operation. */
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);//获取头类型字节数
    if (oldtype==type) {//类型一致,则当前sdshrd长度变量足够存储newlen,则realloc即可
        newsh = s_realloc(sh, hdrlen+newlen+1);//realloc并返回
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
    //不一致,则malloc + memcpy + free

    /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        newsh = s_malloc(hdrlen+newlen+1);//分配新的的内存
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);//将数据拷贝到新的内存
        s_free(sh);//释放老的内存块
        s = (char*)newsh+hdrlen;//s重新指向新的数据块
        s[-1] = type;//赋值类型,这种操作很明白,一看就懂了,往回退1字节,不愧是搞嵌入式的人。
        sdssetlen(s, len);//设置字符串长度,并没有strcat,所以长度不变
    }
    sdssetalloc(s, newlen);//设置分配的内容
    return s;
}

其余API都对应着类似的操作,整个我们已经可很清楚的看懂了。下面一幅图对应着

在sds.c中,几乎所有的函数所传的参数都是sds类型,而非表头sdshdr的地址,但是使用了通过sds指针运算从而求得表头的地址的技巧,因为sds是指向sdshdr结构buf成员,在堆中地址向上增长,所以可以通过指针的算术元算找到表头即可

跳跃表

https://blog.csdn.net/ict2014/article/details/17394259
https://blog.csdn.net/men_wen/article/details/70040026

1、跳跃表(skiplist)介绍
跳跃表是一种有序数据结构,通过每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。其中节点中指针的数目是通过随机生成的,所以跳跃表是使用概率均衡技术而不是使用强制性均衡,因此,对于插入和删除结点比传统上的平衡树算法更为简洁高效。跳跃表支持平均O(LogN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作批处理节点,因此大部分情况下可以和平衡树媲美,其实现起来更为简单。
通过在每个节点中存放多个指向下一个不同节点的指针,实现跳跃的功能。类似于将一系列节点跳跃性的连接成多条有序链表,查找过程就是先从最高层开始,然后遍历所以层次,最后定位到节点。
这里写图片描述
上图是一个4层的跳跃表。将处在相同层次的节点联结起来,于是类似于含有4条跳跃的链表。每个节点中层是随机生成的。随机生成函数可以自己写,

2、随机数生成函数
在redis中,返回一个随机层数值,随机算法所使用的幂次定律。

  • 含义是:如果某件事的发生频率和它的某个属性成幂关系,那么这个频率就可以称之为符合幂次定律。
  • 表现是:少数几个事件的发生频率占了整个发生频率的大部分, 而其余的大多数事件只占整个发生频率的一个小部分。
int zslRandomLevel(void) { //返回一个随机层数值
    int level = 1;
    //(random()&0xFFFF)只保留低两个字节的位值,其他高位全部清零,所以该值范围为0到0xFFFF
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))  //ZSKIPLIST_P(0.25)所以level+1的概率为0.25
        level += 1;         //返回一个1到ZSKIPLIST_MAXLEVEL(32)之间的值
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
#define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */

2、Redis跳跃表

typedef struct zskiplistNode {
    sds ele;                            //保存成员对象动态字符串
    double score;                       //分值
    struct zskiplistNode *backward;     //后退指针,指向前一个节点,方便范围查找
    struct zskiplistLevel {
        struct zskiplistNode *forward;  //前进指针,指向下一个节点
        unsigned int span;              //跨度,此级跨越几个节点
    } level[];                          //层级,是柔型数组,根据随机数动态分配
} zskiplistNode;//跳跃表节点结构体

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;//管理一个跳跃表,记录头部和尾部节点以及跳跃表长度。

通过ZADD操作之后,跳跃表的随机构成可能是下面结构,其中节点中层次是随机生成的。

127.0.0.1:6379> ZADD score 95.5 Mike 98 Li 96 Wang  //socre是一个有序集合键
(integer) 3
127.0.0.1:6379> ZRANGE score 0 -1 WITHSCORES//所有分数按从小到大排列,每一个成员都保存了一个分数
1) "Mike"
2) "95.5"
3) "Wang"
4) "96" 
5) "Li"
6) "98"
127.0.0.1:6379> ZSCORE score Mike       //查询Mike的分值
"95.5"

这里写图片描述
在level数组中,处于同一级的,就必须构造成链表,如上图。只要符合这一规则,那么就可以使用跳跃表了。根据上图,直接讲解Redis中跳跃表操作得API。

//创建一个新的跳跃表,管理跳跃表的结构体
/* Create a new skiplist. */
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;

    zsl = zmalloc(sizeof(*zsl));//分配内存
    zsl->level = 1;//层级初始化0
    zsl->length = 0;//元素个数初始化0
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);//创建一个头结点,可见头结点最高是32层,分数是0,元素是空,这个用来管理链表
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }//将header的下一个节点全部初始化为空且span为0
    zsl->header->backward = NULL;//前节点为空
    zsl->tail = NULL;//尾部为空
    return zsl;//返回链表首地址
}

//创建一个跳跃表节点,实现很简单。
/* Create a skiplist node with the specified number of levels.
 * The SDS string 'ele' is referenced by the node after the call. */
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    zskiplistNode *zn =
        zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
        //柔性数组,可以看到分配的内存是连续的,很方便。
    zn->score = score;//分数
    zn->ele = ele;//元素
    return zn;//返回节点地址。   
    //Redis作者写的代码,真的很符合我的口味,写的没有那么绕口,不愧是搞嵌入式出生的人。
}

//释放一个节点
/* Free the specified skiplist node. The referenced SDS string representation
 * of the element is freed too, unless node->ele is set to NULL before calling
 * this function. */
void zslFreeNode(zskiplistNode *node) {
    sdsfree(node->ele);//先释放动态字符串内存。
    zfree(node);//然后释放当前节点占用内存。
}

//随机获取1到32之间的数字,作为节点中level层级。
/* 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;
}

/*
向跳跃表中插入一个节点:
1、先通过score定位,ele应该所处的位置,并通过update记录每层中节点转向的指针。
2、新键一个节点,并用传入的值初始化。
3、通过前面的update记录的节点信息,将新节点插入对应层次的单向链表。
*/
/* 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;
    //相当于遍历level-1层链表,实现思路很容易
    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];//更新rank[i]为i+1所跨越的节点数,但是最外一层为0
        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)))//后一个节点存在,且分数小于sore,则在当前链表中前进一步。
        {
            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;            //将大于等于原来zsl->level层以上的rank[]设置为0
            update[i] = zsl->header;//将大于等于原来zsl->level层以上update[i]指向头结点
            update[i]->level[i].span = zsl->length;//update[i]已经指向头结点,将第i层的跨度设置为length
        }
        zsl->level = level;
    }//如果随机层数大于跳跃表层,则更新跳跃表zsl
    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;//将节点插入i层的链表,因为update记录了i层上一个节点信息,所以很容易实现。

        /* 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++) {//如果插入节点的level小于原来的zsl->level才会执行
        update[i]->level[i].span++;//因为高度没有达到这些层,所以只需将查找时每层最后一个节点的值的跨度加1
    }

    //设置插入节点的后退指针,就是查找时最下层的最后一个节点,该节点的地址记录在update[0]中
    //如果插入在第二个节点,也就是头结点后的位置就将后退指针设置为NULL
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)//如果x节点不是最尾部的节点
        x->level[0].forward->backward = x;//就将x节点后面的节点的后退节点设置成为x地址
    else
        zsl->tail = x;//否则更新表头的tail指针,指向最尾部的节点x
    zsl->length++;//跳跃表节点计数器加1
    return x;//返回x地址
}



//根据传入的update指针数组,更新跳跃表中每条链表
//被zslDelete, zslDeleteByScore and zslDeleteByRank使用的内部函数
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {  
    int i;
    //设置前进指针和跨度
    for (i = 0; i < zsl->level; i++) {              //遍历下标为0到跳跃表最大层数-1的层
        if (update[i]->level[i].forward == x) {     //如果找到该节点
            update[i]->level[i].span += x->level[i].span - 1;   //将前一个节点的跨度减1
            update[i]->level[i].forward = x->level[i].forward;
            //前一个节点的前进指针指向被删除的节点的后一个节点,跳过该节点
        } else {
            update[i]->level[i].span -= 1;  //在第i层没找到,只将该层的最后一个节点的跨度减1
        }
    }
    //设置后退指针
    if (x->level[0].forward) {      //如果被删除的前进节点不为空,后面还有节点
        x->level[0].forward->backward = x->backward;    //就将后面节点的后退指针指向被删除节点x的回退指针
    } else {
        zsl->tail = x->backward;       //否则直接将被删除的x节点的后退节点设置为表头的tail指针
    }

    //更新跳跃表最大层数
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;
    zsl->length--;  //节点计数器减1
}

//根据分数和元素删除元素
/* Delete an element with matching score/element from the skiplist.
 * The function returns 1 if the node was found and deleted, otherwise
 * 0 is returned.
 *
 * If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise
 * it is not freed (but just unlinked) and *node is set to the node pointer,
 * so that it is possible for the caller to reuse the node (including the
 * referenced SDS string at node->ele). */
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i;

//查找位置
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        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)))
        {
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    /* We may have multiple elements with the same score, what we need
     * is to find the element with both the right score and object. */
    x = x->level[0].forward;//获取当前节点
    if (x && score == x->score && sdscmp(x->ele,ele) == 0) {//确实是这个元素
        zslDeleteNode(zsl, x, update);//更新删除节点之后的跳跃表。
        //zskiplistNode *update[ZSKIPLIST_MAXLEVEL]是数组,类型是zskiplistNode *,所以应该传入其地址,为zskiplistNode * * 类似于int a[12],形参类型是int *。
        if (!node)
            zslFreeNode(x);//释放当前节点信息
        else
            *node = x;
        return 1;
    }
    return 0; /* not found */
}

上述查找、插入、释放的过程。
这里写图片描述

其他API暂时先放放。

整数集合

整数集合(intset)是集合键底层实现之一,以有序、无重复的方式保存集合原色。集合键另一实现是值为空的哈希表(hash table),虽然使用哈希表对集合的加入删除元素,判断元素是否存在等等操作时间复杂度为O(1),但是当存储的元素是整型且元素数目较少时,如果使用散列表存储,就会比较浪费内存,因此整数集合(intset)类型因为节约内存而存在,直接使用柔性数组存储即可。
将元素有序的放在柔性数组中,并且当数据超过当前编码,则会进行数据类型升级并合理迁移先前元素,防止存储溢出,这种实现方式在数据量比较少的时候,很有用。

类型升级的特点

  • 提升灵活性:因为C语言是静态类型的语言,通常在在数组中只是用一种类型保存数据,例如,要么只用int16_t类型,要么只用int32_t类型。通过自动升级底层数组来适应不同类型的新元素,不必担心类型的错误。
  • 节约内存:整数集合既可以让集合保存三种不同类型的值,又可以确保升级操作只在有需要的时候进行,这样就节省了内存。
  • 不支持降级:一旦对数组进行升级,编码就会一直保存升级后的状态。
typedef struct intset {
    uint32_t encoding;//编码方式
    uint32_t length;//集合中元素数量
    int8_t contents[];//柔性数组,扩容通过realloc
    //从小到大存放元素,且不包含重复项
} intset;
//encoding可选范围
#define INTSET_ENC_INT16 (sizeof(int16_t))   //16位,2个字节,表示范围-32,768~32,767
#define INTSET_ENC_INT32 (sizeof(int32_t))   //32位,4个字节,表示范围-2,147,483,648~2,147,483,647
#define INTSET_ENC_INT64 (sizeof(int64_t))   //64位,8个字节,表示范围-9,223,372,036,854,775,808~9,223,372,036,854,775,807


/* Create an empty intset. */
intset *intsetNew(void) {
    intset *is = zmalloc(sizeof(intset));
    is->encoding = intrev32ifbe(INTSET_ENC_INT16);
    is->length = 0;
    return is;
}//创建空的intset集,默认是int16

1、往整数集中pos位置插入元素。

/* Set the value at pos, using the configured encoding. */
//根据集合is设置的编码方式,设置下标为pos的值为value,用指针操作很容易,直接强制转换,然后赋值。
//根据主机字节序用来做内存大小端的转换,主机字节序是大端,如果模板机器的小端,那么需要转换。
static void _intsetSet(intset *is, int pos, int64_t value) {
    uint32_t encoding = intrev32ifbe(is->encoding);

    if (encoding == INTSET_ENC_INT64) {
        ((int64_t*)is->contents)[pos] = value;//直接将地址转换成编码类型,然后通过指针智能设定。
        memrev64ifbe(((int64_t*)is->contents)+pos);//小端转大端存储。
    } else if (encoding == INTSET_ENC_INT32) {
        ((int32_t*)is->contents)[pos] = value;
        memrev32ifbe(((int32_t*)is->contents)+pos);//如果模板机器是小端,则需要小转大端存储。
    } else {
        ((int16_t*)is->contents)[pos] = value;
        memrev16ifbe(((int16_t*)is->contents)+pos);//如果模板机器是小端,则需要小转大端存储。
    }
}

2、类型升级操作
这里写图片描述
上图是需要升级的过程,如果不需要升级,则先扩容将元素通过有序二分查找找到合适的位置,然后在移动后面的元素,插入新的元素。

这种缺陷在于,每次插入元素都需要realloc,并且类型不匹配还需要升级和realloc。
复杂度很垃圾的。优点就在于可以节约内存,不知道为什么作者要使用这种骚东西,搞不明白。一个hash又浪费了多少空间了。

//当需要升级,或者不升级插入新元素,都需要扩容。注意使用的ralloc,这个在很多源代码使用很多。
//https://blog.csdn.net/u010710458/article/details/77274704#t2
//重置数组大小,依据新类型分配空间
static intset *intsetResize(intset *is, uint32_t len) {
    uint32_t size = len*intrev32ifbe(is->encoding);
    is = zrealloc(is,sizeof(intset)+size);//注意柔性数组通过ralloc重新分配。
    return is;
}

//往intset中加入元素.
//如果当前类型足够存储value,则不用升级,直接扩容
//不够存储,则直接升级-->移动元素-->存储
/* Insert an integer in the intset */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    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. */
    if (valenc > intrev32ifbe(is->encoding)) {//编码大,则升级插入
        /* This always succeeds, so we don't need to curry *success. */
        return intsetUpgradeAndAdd(is,value);
    } else {//否则找到合适位置,再移动插入。
        /* 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. */
        if (intsetSearch(is,value,&pos)) {//二分查找定位,如果value已经存在,intsetSearch返回1,如果不存在,pos保存value可以插入的位置
            if (success) *success = 0;//value存在,success设置为0,并返回
            return is;
        }

        //不存在,pos保存可插入位置。
        is = intsetResize(is,intrev32ifbe(is->length)+1);//重新分配足够的空间
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);//不在末尾,则移动pos后面的元素
    }

    _intsetSet(is,pos,value);//新值插入pos
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);//返回长度
    return is;
}

//升级操作,下面用一副图片说明,扩容,然后升级
//首先,需要升级,则必然是当前类型容不下,
//要么太大,要么太小,所以必然存储在扩容之后的前或者尾部。
/* Upgrades the intset to a larger encoding and inserts the given integer. */
//根据value的编码方式,对整数集合is的编码格式升级
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    uint8_t curenc = intrev32ifbe(is->encoding);//先前encoded
    uint8_t newenc = _intsetValueEncoding(value);//当前encoded
    int length = intrev32ifbe(is->length);
    int prepend = value < 0 ? 1 : 0;//小于0,则肯定最小,大于0则肯定最大,必定放在头部或者尾部。

    /* First set new encoding and resize */
    is->encoding = intrev32ifbe(newenc);
    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. */
    while(length--)
        //_intsetGetEncoded(is,length,curenc)根据先前编码获取先前位置值,这个很容易,通过指针获取即可。
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));//将先前元素插入新位置。
        //先放先前最大的,然后第二大的,最后最小的。

    /* Set the value at the beginning or the end. */
    if (prepend)//如果插入最小,则将值插入最前面
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);//最大,插入后面
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);//长度+1
    return is;
}

//注意这里作者使用memmove,免去了没存重叠的叨扰,牛逼得很咯。
static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {
    void *src, *dst;
    uint32_t bytes = intrev32ifbe(is->length)-from;//从from到元素的个数
    uint32_t encoding = intrev32ifbe(is->encoding);

    if (encoding == INTSET_ENC_INT64) {
        src = (int64_t*)is->contents+from;
        dst = (int64_t*)is->contents+to;
        bytes *= sizeof(int64_t);
    } else if (encoding == INTSET_ENC_INT32) {
        src = (int32_t*)is->contents+from;//首地址
        dst = (int32_t*)is->contents+to;//目的地址
        bytes *= sizeof(int32_t);//个数*sizeof等于需要移动的全部字节
    } else {
        src = (int16_t*)is->contents+from;
        dst = (int16_t*)is->contents+to;
        bytes *= sizeof(int16_t);
    }
    //memmove可以保证在重叠的时候,将src指定地方复制到des。
    //https://blog.csdn.net/u010710458/article/details/78887755#t11 讲解了重叠问题
    memmove(dst,src,bytes);//将src开始地址的bytes字节,移动到des开始,des比大于src,所以不会发生覆盖问题。
}

其他API暂时不许解释,这些API已经将这个数据结构讲清楚了。
作者在inset.c里面也写了许多测试代码,哈哈,看来都是需要测试的,作者估计写了好久也测试了好久,才确认这个写得没有问题。主要在于玩指针,玩内存。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有时需要偏执狂

请我喝咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值