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

深碍√TFBOYSˉ_ 2022-05-23 08:05 119阅读 0赞

分析结构体 + 重要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的内存而设计。作者绝对是嵌入式出生,这么爱节约内存。可见作者的功力深厚。结构体的选择会依据字符串的长度动态选择。总之就是分配一块连续的内存块管理字符串。

  1. typedef char *sds;//可代表sdshdr首地址,然后通过地址类型转换访问连续内存块
  2. //sds是为了兼容C字符串设计。
  3. /* Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. */
  4. struct __attribute__ ((__packed__)) sdshdr5 {
  5. unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
  6. char buf[];
  7. };
  8. struct __attribute__ ((__packed__)) sdshdr8 {
  9. uint8_t len; /* used */
  10. uint8_t alloc; /* 数据区域分配的全部内存,除去了head和'/0'。excluding the header and null terminator */
  11. unsigned char flags; /* 高3位代表结构体存储len的数据类型,有了整个参数就可以通过buf正确访问len和alloc了。宏定义就是这样做的。3 lsb of type, 5 unused bits */
  12. char buf[];//柔性数组
  13. };
  14. struct __attribute__ ((__packed__)) sdshdr16 {
  15. uint16_t len; /* used */
  16. uint16_t alloc; /* excluding the header and null terminator */
  17. unsigned char flags; /* 3 lsb of type, 5 unused bits */
  18. char buf[];
  19. };
  20. struct __attribute__ ((__packed__)) sdshdr32 {
  21. uint32_t len; /* used */
  22. uint32_t alloc; /* excluding the header and null terminator */
  23. unsigned char flags; /* 3 lsb of type, 5 unused bits */
  24. char buf[];
  25. };
  26. struct __attribute__ ((__packed__)) sdshdr64 {
  27. uint64_t len; /* used */
  28. uint64_t alloc; /* excluding the header and null terminator */
  29. unsigned char flags; /* 3 lsb of type, 5 unused bits */
  30. char buf[];
  31. };
  32. //强迫症,以上仅仅是为了节约存储len和alloc的内存而设计。
  33. #define SDS_TYPE_5 0
  34. #define SDS_TYPE_8 1
  35. #define SDS_TYPE_16 2
  36. #define SDS_TYPE_32 3
  37. #define SDS_TYPE_64 4
  38. #define SDS_TYPE_MASK 7
  39. #define SDS_TYPE_BITS 3
  40. #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));//宏定义连接符##了解下,变量
  41. #define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))//找出sdshdr16的数据区域
  42. #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)

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

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

  1. /* Create a new sds string starting from a null terminated C string. */
  2. //通过c字符串,创建一个sds。将c字符串复制到sds中,并返回sdshrd中的buf。
  3. sds sdsnew(const char *init) {
  4. size_t initlen = (init == NULL) ? 0 : strlen(init);//需求长度
  5. return sdsnewlen(init, initlen);//分配sdshrd,并返回其中的buf数据区域地址。
  6. }
  7. /* Create a new sds string with the content specified by the 'init' pointer
  8. * and 'initlen'.
  9. * If NULL is used for 'init' the string is initialized with zero bytes.
  10. *
  11. * The string is always null-termined (all the sds strings are, always) so
  12. * even if you create an sds string with:
  13. *
  14. * mystring = sdsnewlen("abc",3);
  15. *
  16. * You can print the string with printf() as there is an implicit \0 at the
  17. * end of the string. However the string is binary safe and can contain
  18. * \0 characters in the middle, as the length is stored in the sds header. */
  19. sds sdsnewlen(const void *init, size_t initlen) {
  20. //创建SDS字符串,直接返回char即可,需要访问SDS,通过指针回溯即可
  21. void *sh;
  22. sds s;
  23. char type = sdsReqType(initlen);//根据输入字符串长度,选择合适的sdshrd,完全为了节约内存。
  24. /* Empty strings are usually created in order to append. Use type 8
  25. * since type 5 is not good at this. */
  26. if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
  27. int hdrlen = sdsHdrSize(type);//头结构体节点占用内存。
  28. unsigned char *fp; /* flags pointer. */
  29. sh = s_malloc(hdrlen+initlen+1);//分配=头结点+数据长度+结束符
  30. if (!init)
  31. memset(sh, 0, hdrlen+initlen+1);//将新分配的sh内存清空。因为malloc并不保证先前位置为空,所以必须设为0
  32. if (sh == NULL) return NULL;
  33. s = (char*)sh+hdrlen;//定位head中的数据区域
  34. fp = ((unsigned char*)s)-1;//sdshdr中的flags域
  35. switch(type) {
  36. case SDS_TYPE_5: {
  37. *fp = type | (initlen << SDS_TYPE_BITS);
  38. break;
  39. }
  40. case SDS_TYPE_8: {
  41. SDS_HDR_VAR(8,s);
  42. sh->len = initlen;
  43. sh->alloc = initlen;
  44. *fp = type;
  45. break;
  46. }
  47. case SDS_TYPE_16: {
  48. SDS_HDR_VAR(16,s);//通过数据域的地址,获取整个sdshrd的地址
  49. sh->len = initlen;//初始化
  50. sh->alloc = initlen;//初始化
  51. *fp = type;//结构体中类型
  52. break;'/0'
  53. }
  54. case SDS_TYPE_32: {
  55. SDS_HDR_VAR(32,s);
  56. sh->len = initlen;
  57. sh->alloc = initlen;
  58. *fp = type;
  59. break;
  60. }
  61. case SDS_TYPE_64: {
  62. SDS_HDR_VAR(64,s);
  63. sh->len = initlen;
  64. sh->alloc = initlen;
  65. *fp = type;
  66. break;
  67. }
  68. }
  69. if (initlen && init)
  70. memcpy(s, init, initlen);//将init数据全部拷贝到s数据区域
  71. s[initlen] = '\0';//结束符
  72. return s;//返回s
  73. }

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

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

  1. /* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
  2. * end of the specified sds string 's'.
  3. *
  4. * After the call, the passed sds string is no longer valid and all the
  5. * references must be substituted with the new pointer returned by the call. */
  6. sds sdscatlen(sds s, const void *t, size_t len) {
  7. size_t curlen = sdslen(s);//获取s长度
  8. s = sdsMakeRoomFor(s,len);//扩容sds,若容量够,则直接拼接,防止溢出
  9. if (s == NULL) return NULL;
  10. memcpy(s+curlen, t, len);//将t直接拷贝到s+curlen位置处。很简单的memcpy。注意内存已分配,绝对不会造成重叠的问题。
  11. sdssetlen(s, curlen+len);//设置字符串长度
  12. s[curlen+len] = '\0';//末尾加上空
  13. return s;//返回s,s可能被修改,也可能没有被修改。
  14. }
  15. /* Enlarge the free space at the end of the sds string so that the caller
  16. * is sure that after calling this function can overwrite up to addlen
  17. * bytes after the end of the string, plus one more byte for nul term.
  18. *
  19. * Note: this does not change the *length* of the sds string as returned
  20. * by sdslen(), but only the free buffer space we have. */
  21. sds sdsMakeRoomFor(sds s, size_t addlen) {
  22. void *sh, *newsh;
  23. size_t avail = sdsavail(s);//返回sds可用容量
  24. size_t len, newlen;
  25. char type, oldtype = s[-1] & SDS_TYPE_MASK;
  26. int hdrlen;
  27. /* Return ASAP if there is enough space left. */
  28. if (avail >= addlen) return s;//可用容量足够放下addlen长度字符串,直接返回
  29. /*
  30. 不够则需要进行扩容加上搬移。
  31. */
  32. len = sdslen(s);//原来字符串长度
  33. sh = (char*)s-sdsHdrSize(oldtype);//找到结构体头部
  34. newlen = (len+addlen);//需要分配新的长度
  35. if (newlen < SDS_MAX_PREALLOC)//新的长度小于1M,则直接重新分配2倍,为以后使用避免重新分配内存
  36. newlen *= 2;
  37. else
  38. newlen += SDS_MAX_PREALLOC;//大于1M,则直接多分配1M
  39. type = sdsReqType(newlen);//获取相应的sdshrd的类型表示。
  40. /* Don't use type 5: the user is appending to the string and type 5 is
  41. * not able to remember empty space, so sdsMakeRoomFor() must be called
  42. * at every appending operation. */
  43. if (type == SDS_TYPE_5) type = SDS_TYPE_8;
  44. hdrlen = sdsHdrSize(type);//获取头类型字节数
  45. if (oldtype==type) {//类型一致,则当前sdshrd长度变量足够存储newlen,则realloc即可
  46. newsh = s_realloc(sh, hdrlen+newlen+1);//realloc并返回
  47. if (newsh == NULL) return NULL;
  48. s = (char*)newsh+hdrlen;
  49. } else {
  50. //不一致,则malloc + memcpy + free
  51. /* Since the header size changes, need to move the string forward,
  52. * and can't use realloc */
  53. newsh = s_malloc(hdrlen+newlen+1);//分配新的的内存
  54. if (newsh == NULL) return NULL;
  55. memcpy((char*)newsh+hdrlen, s, len+1);//将数据拷贝到新的内存
  56. s_free(sh);//释放老的内存块
  57. s = (char*)newsh+hdrlen;//s重新指向新的数据块
  58. s[-1] = type;//赋值类型,这种操作很明白,一看就懂了,往回退1字节,不愧是搞嵌入式的人。
  59. sdssetlen(s, len);//设置字符串长度,并没有strcat,所以长度不变
  60. }
  61. sdssetalloc(s, newlen);//设置分配的内容
  62. return s;
  63. }

其余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) { //返回一个随机层数值

    1. int level = 1;
    2. //(random()&0xFFFF)只保留低两个字节的位值,其他高位全部清零,所以该值范围为0到0xFFFF
    3. while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) //ZSKIPLIST_P(0.25)所以level+1的概率为0.25
    4. level += 1; //返回一个1到ZSKIPLIST_MAXLEVEL(32)之间的值
    5. 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跳跃表

  1. typedef struct zskiplistNode {
  2. sds ele; //保存成员对象动态字符串
  3. double score; //分值
  4. struct zskiplistNode *backward; //后退指针,指向前一个节点,方便范围查找
  5. struct zskiplistLevel {
  6. struct zskiplistNode *forward; //前进指针,指向下一个节点
  7. unsigned int span; //跨度,此级跨越几个节点
  8. } level[]; //层级,是柔型数组,根据随机数动态分配
  9. } zskiplistNode;//跳跃表节点结构体
  10. typedef struct zskiplist {
  11. struct zskiplistNode *header, *tail;
  12. unsigned long length;
  13. int level;
  14. } zskiplist;//管理一个跳跃表,记录头部和尾部节点以及跳跃表长度。

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

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

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

  1. //创建一个新的跳跃表,管理跳跃表的结构体
  2. /* Create a new skiplist. */
  3. zskiplist *zslCreate(void) {
  4. int j;
  5. zskiplist *zsl;
  6. zsl = zmalloc(sizeof(*zsl));//分配内存
  7. zsl->level = 1;//层级初始化0
  8. zsl->length = 0;//元素个数初始化0
  9. zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);//创建一个头结点,可见头结点最高是32层,分数是0,元素是空,这个用来管理链表
  10. for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
  11. zsl->header->level[j].forward = NULL;
  12. zsl->header->level[j].span = 0;
  13. }//将header的下一个节点全部初始化为空且span为0
  14. zsl->header->backward = NULL;//前节点为空
  15. zsl->tail = NULL;//尾部为空
  16. return zsl;//返回链表首地址
  17. }
  18. //创建一个跳跃表节点,实现很简单。
  19. /* Create a skiplist node with the specified number of levels. * The SDS string 'ele' is referenced by the node after the call. */
  20. zskiplistNode *zslCreateNode(int level, double score, sds ele) {
  21. zskiplistNode *zn =
  22. zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
  23. //柔性数组,可以看到分配的内存是连续的,很方便。
  24. zn->score = score;//分数
  25. zn->ele = ele;//元素
  26. return zn;//返回节点地址。
  27. //Redis作者写的代码,真的很符合我的口味,写的没有那么绕口,不愧是搞嵌入式出生的人。
  28. }
  29. //释放一个节点
  30. /* 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. */
  31. void zslFreeNode(zskiplistNode *node) {
  32. sdsfree(node->ele);//先释放动态字符串内存。
  33. zfree(node);//然后释放当前节点占用内存。
  34. }
  35. //随机获取1到32之间的数字,作为节点中level层级。
  36. /* 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. */
  37. int zslRandomLevel(void) {
  38. int level = 1;
  39. while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
  40. level += 1;
  41. return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
  42. }
  43. /* 向跳跃表中插入一个节点: 1、先通过score定位,ele应该所处的位置,并通过update记录每层中节点转向的指针。 2、新键一个节点,并用传入的值初始化。 3、通过前面的update记录的节点信息,将新节点插入对应层次的单向链表。 */
  44. /* 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'. */
  45. zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
  46. zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
  47. unsigned int rank[ZSKIPLIST_MAXLEVEL];
  48. int i, level;
  49. serverAssert(!isnan(score));
  50. x = zsl->header;
  51. //相当于遍历level-1层链表,实现思路很容易
  52. for (i = zsl->level-1; i >= 0; i--) {
  53. /* store rank that is crossed to reach the insert position */
  54. rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];//更新rank[i]为i+1所跨越的节点数,但是最外一层为0
  55. while (x->level[i].forward &&
  56. (x->level[i].forward->score < score ||
  57. (x->level[i].forward->score == score &&
  58. sdscmp(x->level[i].forward->ele,ele) < 0)))//后一个节点存在,且分数小于sore,则在当前链表中前进一步。
  59. {
  60. rank[i] += x->level[i].span;//记录该层一共跨越了多少节点 加上 上一层遍历所跨越的节点数
  61. x = x->level[i].forward;//更新并指向下一个节点
  62. }
  63. update[i] = x;
  64. //记录遍历过程中每条链表中跳转的节点,用于分析
  65. }
  66. /* 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. */
  67. level = zslRandomLevel();//获得一个随机的层数
  68. if (level > zsl->level) {
  69. //如果大于当前所有节点最大的层数时
  70. for (i = zsl->level; i < level; i++) {
  71. rank[i] = 0; //将大于等于原来zsl->level层以上的rank[]设置为0
  72. update[i] = zsl->header;//将大于等于原来zsl->level层以上update[i]指向头结点
  73. update[i]->level[i].span = zsl->length;//update[i]已经指向头结点,将第i层的跨度设置为length
  74. }
  75. zsl->level = level;
  76. }//如果随机层数大于跳跃表层,则更新跳跃表zsl
  77. x = zslCreateNode(level,score,ele);//创建一个节点
  78. for (i = 0; i < level; i++) {
  79. //遍历每一层,并更新每一层的链表
  80. x->level[i].forward = update[i]->level[i].forward;
  81. update[i]->level[i].forward = x;//将节点插入i层的链表,因为update记录了i层上一个节点信息,所以很容易实现。
  82. /* update span covered by update[i] as x is inserted here */
  83. x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);//更新插入节点的跨度值
  84. update[i]->level[i].span = (rank[0] - rank[i]) + 1;//更新插入节点前一个节点的跨度值
  85. }
  86. /* increment span for untouched levels */
  87. for (i = level; i < zsl->level; i++) {
  88. //如果插入节点的level小于原来的zsl->level才会执行
  89. update[i]->level[i].span++;//因为高度没有达到这些层,所以只需将查找时每层最后一个节点的值的跨度加1
  90. }
  91. //设置插入节点的后退指针,就是查找时最下层的最后一个节点,该节点的地址记录在update[0]中
  92. //如果插入在第二个节点,也就是头结点后的位置就将后退指针设置为NULL
  93. x->backward = (update[0] == zsl->header) ? NULL : update[0];
  94. if (x->level[0].forward)//如果x节点不是最尾部的节点
  95. x->level[0].forward->backward = x;//就将x节点后面的节点的后退节点设置成为x地址
  96. else
  97. zsl->tail = x;//否则更新表头的tail指针,指向最尾部的节点x
  98. zsl->length++;//跳跃表节点计数器加1
  99. return x;//返回x地址
  100. }
  101. //根据传入的update指针数组,更新跳跃表中每条链表
  102. //被zslDelete, zslDeleteByScore and zslDeleteByRank使用的内部函数
  103. void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
  104. int i;
  105. //设置前进指针和跨度
  106. for (i = 0; i < zsl->level; i++) { //遍历下标为0到跳跃表最大层数-1的层
  107. if (update[i]->level[i].forward == x) { //如果找到该节点
  108. update[i]->level[i].span += x->level[i].span - 1; //将前一个节点的跨度减1
  109. update[i]->level[i].forward = x->level[i].forward;
  110. //前一个节点的前进指针指向被删除的节点的后一个节点,跳过该节点
  111. } else {
  112. update[i]->level[i].span -= 1; //在第i层没找到,只将该层的最后一个节点的跨度减1
  113. }
  114. }
  115. //设置后退指针
  116. if (x->level[0].forward) { //如果被删除的前进节点不为空,后面还有节点
  117. x->level[0].forward->backward = x->backward; //就将后面节点的后退指针指向被删除节点x的回退指针
  118. } else {
  119. zsl->tail = x->backward; //否则直接将被删除的x节点的后退节点设置为表头的tail指针
  120. }
  121. //更新跳跃表最大层数
  122. while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
  123. zsl->level--;
  124. zsl->length--; //节点计数器减1
  125. }
  126. //根据分数和元素删除元素
  127. /* 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). */
  128. int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
  129. zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
  130. int i;
  131. //查找位置
  132. x = zsl->header;
  133. for (i = zsl->level-1; i >= 0; i--) {
  134. while (x->level[i].forward &&
  135. (x->level[i].forward->score < score ||
  136. (x->level[i].forward->score == score &&
  137. sdscmp(x->level[i].forward->ele,ele) < 0)))
  138. {
  139. x = x->level[i].forward;
  140. }
  141. update[i] = x;
  142. }
  143. /* We may have multiple elements with the same score, what we need * is to find the element with both the right score and object. */
  144. x = x->level[0].forward;//获取当前节点
  145. if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
  146. //确实是这个元素
  147. zslDeleteNode(zsl, x, update);//更新删除节点之后的跳跃表。
  148. //zskiplistNode *update[ZSKIPLIST_MAXLEVEL]是数组,类型是zskiplistNode *,所以应该传入其地址,为zskiplistNode * * 类似于int a[12],形参类型是int *。
  149. if (!node)
  150. zslFreeNode(x);//释放当前节点信息
  151. else
  152. *node = x;
  153. return 1;
  154. }
  155. return 0; /* not found */
  156. }

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

其他API暂时先放放。

整数集合

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

类型升级的特点

  • 提升灵活性:因为C语言是静态类型的语言,通常在在数组中只是用一种类型保存数据,例如,要么只用int16_t类型,要么只用int32_t类型。通过自动升级底层数组来适应不同类型的新元素,不必担心类型的错误。
  • 节约内存:整数集合既可以让集合保存三种不同类型的值,又可以确保升级操作只在有需要的时候进行,这样就节省了内存。
  • 不支持降级:一旦对数组进行升级,编码就会一直保存升级后的状态。

    typedef struct intset {

    1. uint32_t encoding;//编码方式
    2. uint32_t length;//集合中元素数量
    3. int8_t contents[];//柔性数组,扩容通过realloc
    4. //从小到大存放元素,且不包含重复项

    } 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

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

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

  1. /* Set the value at pos, using the configured encoding. */
  2. //根据集合is设置的编码方式,设置下标为pos的值为value,用指针操作很容易,直接强制转换,然后赋值。
  3. //根据主机字节序用来做内存大小端的转换,主机字节序是大端,如果模板机器的小端,那么需要转换。
  4. static void _intsetSet(intset *is, int pos, int64_t value) {
  5. uint32_t encoding = intrev32ifbe(is->encoding);
  6. if (encoding == INTSET_ENC_INT64) {
  7. ((int64_t*)is->contents)[pos] = value;//直接将地址转换成编码类型,然后通过指针智能设定。
  8. memrev64ifbe(((int64_t*)is->contents)+pos);//小端转大端存储。
  9. } else if (encoding == INTSET_ENC_INT32) {
  10. ((int32_t*)is->contents)[pos] = value;
  11. memrev32ifbe(((int32_t*)is->contents)+pos);//如果模板机器是小端,则需要小转大端存储。
  12. } else {
  13. ((int16_t*)is->contents)[pos] = value;
  14. memrev16ifbe(((int16_t*)is->contents)+pos);//如果模板机器是小端,则需要小转大端存储。
  15. }
  16. }

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

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

  1. //当需要升级,或者不升级插入新元素,都需要扩容。注意使用的ralloc,这个在很多源代码使用很多。
  2. //https://blog.csdn.net/u010710458/article/details/77274704#t2
  3. //重置数组大小,依据新类型分配空间
  4. static intset *intsetResize(intset *is, uint32_t len) {
  5. uint32_t size = len*intrev32ifbe(is->encoding);
  6. is = zrealloc(is,sizeof(intset)+size);//注意柔性数组通过ralloc重新分配。
  7. return is;
  8. }
  9. //往intset中加入元素.
  10. //如果当前类型足够存储value,则不用升级,直接扩容
  11. //不够存储,则直接升级-->移动元素-->存储
  12. /* Insert an integer in the intset */
  13. intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
  14. uint8_t valenc = _intsetValueEncoding(value);
  15. uint32_t pos;
  16. if (success) *success = 1;
  17. /* 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. */
  18. if (valenc > intrev32ifbe(is->encoding)) {
  19. //编码大,则升级插入
  20. /* This always succeeds, so we don't need to curry *success. */
  21. return intsetUpgradeAndAdd(is,value);
  22. } else {
  23. //否则找到合适位置,再移动插入。
  24. /* 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. */
  25. if (intsetSearch(is,value,&pos)) {
  26. //二分查找定位,如果value已经存在,intsetSearch返回1,如果不存在,pos保存value可以插入的位置
  27. if (success) *success = 0;//value存在,success设置为0,并返回
  28. return is;
  29. }
  30. //不存在,pos保存可插入位置。
  31. is = intsetResize(is,intrev32ifbe(is->length)+1);//重新分配足够的空间
  32. if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);//不在末尾,则移动pos后面的元素
  33. }
  34. _intsetSet(is,pos,value);//新值插入pos
  35. is->length = intrev32ifbe(intrev32ifbe(is->length)+1);//返回长度
  36. return is;
  37. }
  38. //升级操作,下面用一副图片说明,扩容,然后升级
  39. //首先,需要升级,则必然是当前类型容不下,
  40. //要么太大,要么太小,所以必然存储在扩容之后的前或者尾部。
  41. /* Upgrades the intset to a larger encoding and inserts the given integer. */
  42. //根据value的编码方式,对整数集合is的编码格式升级
  43. static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
  44. uint8_t curenc = intrev32ifbe(is->encoding);//先前encoded
  45. uint8_t newenc = _intsetValueEncoding(value);//当前encoded
  46. int length = intrev32ifbe(is->length);
  47. int prepend = value < 0 ? 1 : 0;//小于0,则肯定最小,大于0则肯定最大,必定放在头部或者尾部。
  48. /* First set new encoding and resize */
  49. is->encoding = intrev32ifbe(newenc);
  50. is = intsetResize(is,intrev32ifbe(is->length)+1);//根据新的编码方式重新设置内存空间大小
  51. /* 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. */
  52. while(length--)
  53. //_intsetGetEncoded(is,length,curenc)根据先前编码获取先前位置值,这个很容易,通过指针获取即可。
  54. _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));//将先前元素插入新位置。
  55. //先放先前最大的,然后第二大的,最后最小的。
  56. /* Set the value at the beginning or the end. */
  57. if (prepend)//如果插入最小,则将值插入最前面
  58. _intsetSet(is,0,value);
  59. else
  60. _intsetSet(is,intrev32ifbe(is->length),value);//最大,插入后面
  61. is->length = intrev32ifbe(intrev32ifbe(is->length)+1);//长度+1
  62. return is;
  63. }
  64. //注意这里作者使用memmove,免去了没存重叠的叨扰,牛逼得很咯。
  65. static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {
  66. void *src, *dst;
  67. uint32_t bytes = intrev32ifbe(is->length)-from;//从from到元素的个数
  68. uint32_t encoding = intrev32ifbe(is->encoding);
  69. if (encoding == INTSET_ENC_INT64) {
  70. src = (int64_t*)is->contents+from;
  71. dst = (int64_t*)is->contents+to;
  72. bytes *= sizeof(int64_t);
  73. } else if (encoding == INTSET_ENC_INT32) {
  74. src = (int32_t*)is->contents+from;//首地址
  75. dst = (int32_t*)is->contents+to;//目的地址
  76. bytes *= sizeof(int32_t);//个数*sizeof等于需要移动的全部字节
  77. } else {
  78. src = (int16_t*)is->contents+from;
  79. dst = (int16_t*)is->contents+to;
  80. bytes *= sizeof(int16_t);
  81. }
  82. //memmove可以保证在重叠的时候,将src指定地方复制到des。
  83. //https://blog.csdn.net/u010710458/article/details/78887755#t11 讲解了重叠问题
  84. memmove(dst,src,bytes);//将src开始地址的bytes字节,移动到des开始,des比大于src,所以不会发生覆盖问题。
  85. }

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

发表评论

表情:
评论列表 (有 0 条评论,119人围观)

还没有评论,来说两句吧...

相关阅读

    相关 redis 跳跃

    跳跃表 跳跃表是有序集合的底层实现之一, 除此之外它在 Redis 中没有其他应用。 Redis 的跳跃表实现由 zskiplist 和 zskiplistN

    相关 redis源码学习跳跃

    跳跃表 跳跃表对于我来说是一个比较陌生的数据结构,因此花了一上午的时间先看了一蛤MIT的公开课。[网易云课堂——MIT跳跃表][MIT] 什么是跳跃表,有一个很简单的例