近来在看黄健宏的《Redis设计与实现》,读到整数集合这一节,其中有一些细节问题没有读到。
在互联网上也没有搜到答案,于是看源码有所得,于此记录一下同时也作为分享。
《Redis设计与实现》第一版发布于2014年4月,所用的redis版本是2.9, 比较旧,有些内容与最新版本也有所不同,下文也有提及。
对于书中原文提到的内容可以参考此博客: www.cnblogs.com/panxianhao/..., 与书中分毫不差。
同时在这里感谢黄先生对Redis源码的分享。
整数集合 (intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。 举个例子,如果我们创建一个只包含五个元素的集合键,并且集合中的所有元素都是整数值,那么这个集合键的底层实现就会是整数集合
书中原文是这样提的,但是下载Redis源码之后,发现在2023年11月30日下载的redis-7.2.3中,intset中可存储数据的大小限制变为1GB:
c
// src/t_set.c
/* Return the maximum number of entries to store in an intset. */
static size_t intsetMaxEntries(void) {
size_t max_entries = server.set_max_intset_entries;
/* limit to 1G entries due to intset internals. */
if (max_entries >= 1<<30) max_entries = 1<<30;
return max_entries;
}
并且在数据过多,超过上限时转换为HT(hashTable)
c
src/t_set.c
/* Converts intset to HT if it contains too many entries. */
static void maybeConvertIntset(robj *subject) {
serverAssert(subject->encoding == OBJ_ENCODING_INTSET);
if (intsetLen(subject->ptr) > intsetMaxEntries())
setTypeConvert(subject,OBJ_ENCODING_HT);
}
整数集合的数据结构:
c++
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
encoding表示contents中存储数据的类型,有如下定义:
c
/* Note that these encodings are ordered, so:
* INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
contents虽然类型是int8_t,但不会存储int8_t的数据,相当于是字节组成的数组吧 具体赋值插入时的算法如下,会把这个数组转换为具体的类型
c
//intset.c
/* Set the value at pos, using the configured encoding. */
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);
}
}
升级与扩容:
当新加入的数据的类型占据的内存大小比之前的大时,会升级contents中元素的类型 比如原来有3个int16_t的数据,现在添加了一个int32类型的值,那就会把整个数组中的元素升级为int32_t,然后再插入
扩容原文没有提, 查看源码如下:
c
// intset.c
is = intsetResize(is,intrev32ifbe(is->length)+1);
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
/* Resize the intset */
static intset *intsetResize(intset *is, uint32_t len) {
uint64_t size = (uint64_t)len*intrev32ifbe(is->encoding);
assert(size <= SIZE_MAX - sizeof(intset));
is = zrealloc(is,sizeof(intset)+size);
return is;
}
每次只新增一个元素大小的空间,然后把原来的数据进行移动(根据新插入元素的pos)
注意: intset不支持降级
添加与删除元素的时间复杂度:
都是O(N), 因为要移动元素,再调整空间大小
c
//intset.c
/* Delete integer from 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);
/* We know we can delete */
if (success) *success = 1;
/* Overwrite value with tail and update length */
if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
is = intsetResize(is,len-1);
is->length = intrev32ifbe(len-1);
}
return is;
}
书中的重点回顾此作为记录:
- 整数集合是集合键的底层实现之一
- 以有序、无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型
- 升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存
- 整数集合只支持升级操作,不支持降级操作