Redis数据结构-IntSet

《Reids数据结构》这篇文章中,我们说到了Redis中的Set数据类型底层是使用的intset或者hashtable数据结构,本文简单说下intset,关于hashtable可以看看这篇文章:《Redis数据结构-HashTable》

Redis在什么时候使用intset呢?

简单说如果Set中的元素都是整数类型且元素个数小于等于配置项set-max-intset-entries,那就用intset,反之hashtable

下面我们将set-max-intset-entries修改为3方便测试:

shell
复制代码
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
127.0.0.1:6379> config set set-max-intset-entries 3 OK 127.0.0.1:6379> SADD k1 1 2 3 (integer) 3 127.0.0.1:6379> OBJECT ENCODING k1 "intset" 127.0.0.1:6379> SADD intset 4 (integer) 1 127.0.0.1:6379> OBJECT ENCODING intset "hashtable"

可以看到当k1的元素个数超过3之后,编码方式从intset变为了hashtable

同理集合中包含非整数元素也会使用hashtable

shell
复制代码
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
127.0.0.1:6379> config set set-max-intset-entries 3 OK 127.0.0.1:6379> SADD k2 1 2 a (integer) 3 127.0.0.1:6379> OBJECT ENCODING k2 "hashtable"

结构设计

在源码intset.h第35行可以看到intset的结构是这样的:

c
复制代码
  • 01
  • 02
  • 03
  • 04
  • 05
typedef struct intset { uint32_t encoding; uint32_t length; int8_t contents[]; } intset;
  1. encoding:编码方式,共支持三种范围:
    1. INTSET_ENC_INT16:占用2个字节,存储范围为[-2^16^,2^16^-1]
    2. INTSET_ENC_INT32:占用4个字节,存储范围为[-2^32^,2^32^-1]
    3. INTSET_ENC_INT64:占用8个字节,存储范围为[-2^64^,2^64^-1]
  2. length:存储元素个数。
  3. contents[]:指向实际存储数值的连续内存区域(也就是一个数组),intset的每个元素都是contents[]数组的一个数组项,每个项在数组中升序排列,且数组中不包含任何重复项。(虽然这里将contents[]属性声明为int8_t类型的数组,但实际上contents[]数组并不保存任何int8_t类型的值,contents[]数组的真正类型取决于encoding属性的值)。

常用操作

intset的升级(扩容)

前面说到了contents[]的元素类型是由encoding来决定的,那么假设当前contents[]里的元素类型是int32INTSET_ENC_INT32),然后插入一个新的数据,但是类型是int64INTSET_ENC_INT64),会发生什么呢?

在源码intset.c第159行有个函数intsetUpgradeAndAdd,这个函数就是用来执行intset的升级的:

c
复制代码
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
/* 升级intset并插入给定整数。 */ static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { /* 当前的编码方式 */ uint8_t curenc = intrev32ifbe(is->encoding); /* 使用要添加的新值获取新的编码方式 */ uint8_t newenc = _intsetValueEncoding(value); int length = intrev32ifbe(is->length); /* 确定数据添加的位置 */ int prepend = value < 0 ? 1 : 0; /* 首先设置新的类型并调整大小 */ is->encoding = intrev32ifbe(newenc); is = intsetResize(is,intrev32ifbe(is->length)+1); /* 从后往前升级,这样就不会覆盖值。比如原本集合有1、2、3、4、5、6INTSET_ENC_INT16,新加入的65538则需要用INTSET_ENC_INT32。 * 注意:“prepend”变量用于确保intset的开头或结尾有一个空的位置。 */ while(length--) _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc)); /* 将待添加的value添加到首部或者尾部, 因为是扩容所以value是大于原有最大值或小于最小值。 */ if (prepend) _intsetSet(is,0,value); else _intsetSet(is,intrev32ifbe(is->length),value); /* 长度加1*/ is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; }

如果在一个int32类型的intset中新增一个int64类型的值,那么整个intset里面的元素类型都会变成int64,这个过程主要分为三步:

  1. 根据新增的元素的类型,扩展contents[]的空间大小,并为新元素分配空间。
  2. contents[]当前的所有元素,都转换为和新增元素相同的类型,然后将类型转换后的元素放置到正确的位上,且在放置的过程中还要保证原本数组的有序性。
  3. 最后修改encoding以及length长度加1。

注意:intset升级后不会再次降级,这里主要是考虑到资源消耗问题,没有必要在降级。

添加

intset的添加元素操作的源码在intset.c第206行*intsetAdd函数:

C
复制代码
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
/* 在 intset 中插入一个整数 */ intset *intsetAdd(intset *is, int64_t value, uint8_t *success) { /*获取合适的编码方式*/ uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 1; /* 如果需要升级编码,则进行升级。 * 如果我们需要升级,我们知道这个值应该被追加(如果 > 0)或预置(如果 < 0), * 因为它在现有值的范围之外。 */ if (valenc > intrev32ifbe(is->encoding)) { /* 这里执行intset的升级以及添加 */ return intsetUpgradeAndAdd(is,value); } else { /* 如果集合中已经存在值,则中止。使用了二分查询。 * 这个调用将使用正确的位置填充“pos”,以便在找不到值时插入值。*/ if (intsetSearch(is,value,&pos)) { if (success) *success = 0; return is; } /*调整集合大小*/ is = intsetResize(is,intrev32ifbe(is->length)+1); if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1); } /* 真正执行写入集合的函数 */ _intsetSet(is,pos,value); /* 长度加1*/ is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; }

删除

删除操作的源码在intset.c第236行intsetRemove函数中:

c
复制代码
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
/* 从intset中删除整数 */ intset *intsetRemove(intset *is, int64_t value, int *success) { uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 0; /* 当给定值处于当前集合的编码范围且存在集合当中 */ if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) { uint32_t len = intrev32ifbe(is->length); /* 可以删除 */ if (success) *success = 1; /* 用尾部覆盖值并更新长度 */ if (pos < (len-1)) intsetMoveTail(is,pos+1,pos); is = intsetResize(is,len-1); /*长度减1*/ is->length = intrev32ifbe(len-1); } return is; }

查找

一般查找操作(比如返回一个元素)、判断是否存在、查找位置的操作都是调用intset.c第56行的函数_intsetGetEncoded

c
复制代码
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
/* 返回给定编码的位置pos的值 */ static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) { int64_t v64; int32_t v32; int16_t v16; if (enc == INTSET_ENC_INT64) { memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64)); memrev64ifbe(&v64); return v64; } else if (enc == INTSET_ENC_INT32) { memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32)); memrev32ifbe(&v32); return v32; } else { memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16)); memrev16ifbe(&v16); return v16; } }

创建一个intset

要创建一个intset,调用的是intset.c第98行中的*intsetNew函数:

c
复制代码
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
/* 创建一个空的intset */ intset *intsetNew(void) { /* 分配内存 */ intset *is = zmalloc(sizeof(intset)); /* 默认编码是INTSET_ENC_INT16 */ is->encoding = intrev32ifbe(INTSET_ENC_INT16); /* 长度初始为0 */ is->length = 0; return is; }