ntset 和 dict 都是 sadd 命令的底层数据结构,当添加的所有数据都是整数时,会使用前者;否则使用后者。特别的,当遇到添加数据为字符串,即不能表示为整数时,redis 会把数据结构转换为 dict,即把 intset 中的数据全部搬迁到 dict。
整数集合的应用
Intset 是set的底层实现之一,如果一个集合:
1. 只保存着整数元素;
2. 元素的数量不多;
那么 Redis 就会使用 intset 来保存集合元素
intset 结构体
intset 底层本质是一个有序的、不重复的、整型的数组,支持不同类型整数
1: typedef struct intset {2: // 每个整数的类型
3: uint32_t encoding;4:5: // intset 长度
6: uint32_t length;7:8: // 整数数组
9: int8_t contents[];
10: } intset;
encoding 的值可以是以下三个常量的其中一个 :
# define INTSET_ENC_INT16 (sizeof(int16_t))
# define INTSET_ENC_INT32 (sizeof(int32_t))
# define INTSET_ENC_INT64 (sizeof(int64_t))
contents 数组是实际保存元素的地方,数组中的元素有以下两个特性:
• 没有重复元素;
• 元素在数组中从小到大排列;
contents 数组的 int8_t 类型声明比较容易让人误解,实际上,intset 并不使用 int8_t 类型来保存任何元素,结构中的这个类型声明只是作为一个占位符使用:在对 contents 中的元素进行读取或者写入时,程序并不是直接使用 contents 来对元素进行索引,而是根据 encoding的值,对 contents 进行类型转换和指针运算,计算出元素在内存中的正位置。在添加新元素,进行内存分配时,分配的容量也是由 encoding 的值决定。
inset操作
升级
在添加新元素时,如果 intsetAdd 发现新元素不能用现有的编码方式来保存,它就会将升级集合和添加新元素的任务转交给 intsetUpgradeAndAdd 来完成
intsetUpgradeAndAdd 需要完成以下几个任务:
1. 对新元素进行检测,看保存这个新元素需要什么类型的编码;
2. 将集合 encoding 属性的值设置为新编码类型,并根据新编码类型,对整个 contents 数
组进行内存重分配。
3. 调整 contents 数组内原有元素在内存中的排列方式,让它们从旧编码调整为新编码。
4. 将新元素添加到集合中。
整个过程中,最复杂的就是第三步,让我们用一个例子来理解这个步
假设有一个 intset ,里面包含三个用 int16_t 方式保存的数值,分别是 1 、2 和 3 ,它的结
构如下:
intset->encoding = INTSET_ENC_INT16;
intset->length = 3;
intset->contents = [1, 2, 3];
其中,intset->contents 在内存中的排列如下:
现在,我们要要将一个长度为 int32_t 的值 65535 加入到集合中,intset 需要执行以下步骤:
1. 将 encoding 属性设置为 INTSET_ENC_INT32 。
2. 根据 encoding 属性的值,对 contents 数组进行内存重分配。
重分配完成之后,contents 在内存中的排列如下:
contents 数组现在共有可容纳 4 个 int32_t 值的空间。
3. 因为原来的 3 个 int16_t 值还“挤在”contents 前面的 48 个位里,所以程序需要对它们
进行移动和类型转换,从而让它们适应集合的新编码方式。
首先是移动 3
4. 最后,将新值 65535 添加到数组:
intset 搜索
1: static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {2: int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
3: int64_t cur = -1;
4:5: /* The value can never be found when the set is empty */
6: // 集合为空
7: if (intrev32ifbe(is->length) == 0) {
8: if (pos) *pos = 0;
9: return 0;
10: } else {
11: /* Check for the case where we know we cannot find the value,
12: * but do know the insert position. */13: // value 比最大元素还大
14: if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
15: if (pos) *pos = intrev32ifbe(is->length);
16: return 0;
17: // value 比最小元素还小
18: } else if (value < _intsetGet(is,0)) {19: if (pos) *pos = 0;
20: return 0;
21: }22: }23:24: // 二分查找
25: while(max >= min) {
26: mid = (min+max)/2;27: cur = _intsetGet(is,mid);28: if (value > cur) {
29: min = mid+1;30: } else if (value < cur) {31: max = mid-1;32: } else {
33: break;
34: }35: }36:37: if (value == cur) {
38: if (pos) *pos = mid;
39: return 1;
40: } else {
41: if (pos) *pos = min;
42: return 0;
43: }44: }
intset 插入
1: /* Insert an integer in the intset */
2: intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
3: uint8_t valenc = _intsetValueEncoding(value);4: uint32_t pos;5: if (success) *success = 1;
6:7: /* Upgrade encoding if necessary. If we need to upgrade, we know that
8: * this value should be either appended (if > 0) or prepended (if < 0),9: * because it lies outside the range of existing values. */10: // 需要插入整数的所需内存超出了原有集合整数的范围,即内存类型不同,
11: // 则升级整数类型
12: if (valenc > intrev32ifbe(is->encoding)) {
13: /* This always succeeds, so we don't need to curry *success. */
14: return intsetUpgradeAndAdd(is,value);
15:16: // 正常,分配内存,插入
17: } else {
18: // intset 内部不允许重复
19: /* Abort if the value is already present in the set.
20: * This call will populate "pos" with the right position to insert21: * the value when it cannot be found. */22: if (intsetSearch(is,value,&pos)) {
23: if (success) *success = 0;
24: return is;
25: }26:27: // realloc
28: is = intsetResize(is,intrev32ifbe(is->length)+1);29:30: // 迁移内存,腾出空间给新的数据。intsetMoveTail() 完成内存迁移工作
31: if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
32: }33:34: // 在腾出的空间中设置新的数据
35: _intsetSet(is,pos,value);36:37: // 更新 intset size
38: is->length = intrev32ifbe(intrev32ifbe(is->length)+1);39: return is;
40: }41:42: // 升级整数类型,譬如从 short->int。当插入数据的内存占用比原有数据大
43: // 的时候,会被调用
44: /* Upgrades the intset to a larger encoding and inserts the given integer. */
45: static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {46: uint8_t curenc = intrev32ifbe(is->encoding);47: uint8_t newenc = _intsetValueEncoding(value);48: int length = intrev32ifbe(is->length);
49:50: // value<0 头插,value>0 尾插
51: int prepend = value < 0 ? 1 : 0;
52:53: // realloc
54: /* First set new encoding and resize */
55: is->encoding = intrev32ifbe(newenc);56: is = intsetResize(is,intrev32ifbe(is->length)+1);57:58: // 逆向处理,防止数据被覆盖,一般的插入排序步骤
59: /* Upgrade back-to-front so we don't overwrite values.
60: * Note that the "prepend" variable is used to make sure we have an empty61: * space at either the beginning or the end of the intset. */62: while(length--)
63: _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));64:65: // value<0 放在集合开头,否则放在集合末尾。
66: // 因为,此函数是对整数所占内存进行升级,意味着 value 不是在集合中最大就是最小!
67: /* Set the value at the beginning or the end. */
68: if (prepend)
69: _intsetSet(is,0,value);70: else
71: _intsetSet(is,intrev32ifbe(is->length),value);72:73: // 更新 set size
74: is->length = intrev32ifbe(intrev32ifbe(is->length)+1);75: return is;
76: }