上个篇章回顾,我们上个章节我们学习了《Redis从入门到精通【高阶篇】之底层数据结构字典(Dictionary)详解》,我们从源码层了解字典是一种以键值对(key-value)形式存储数据的数据结构。在 Redis 中,字典使用哈希表来实现。哈希表是一种以常数时间复杂度 O(1) 进行插入、删除和查找的数据结构。了解到在 Redis 中,字典被广泛应用于实现哈希表和集合等数据结构。
本章节,我们详细了解一下在Redis又一个底层数据结构整数集(IntSet),它是用于存储整型数据,是一种紧凑的、高效的数据结构,可以用来实现集合等功能。
当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。
整数集的实现方式与普通的数组和链表不同,它采用了一种特殊的压缩算法,可以在尽可能少的内存空间中存储大量的整型数据。在Redis中,整数集通常用来存储集合中的元素,例如有序集合中的分值。
整数集的结构示意如下所示:
+--------+--------+--------+--------+--------+--------+
| header | data | data | data | data | data |
+--------+--------+--------+--------+--------+--------+
整数集由一个头部和多个数据块组成。头部中存储了整数集的元素个数、编码方式和数据块的起始地址等信息。数据块中存储了实际的整型数据。
IntSet的设计目标是尽可能地节省内存空间,同时保证高效的操作性能。它可以存储三种类型的整数:8位整数、16位整数和32位整数。IntSet的内部结构示意如下图所示:
| 32位无符号整数 | 16位无符号整数 | 8位无符号整数 | 8位标志位 |
IntSet由一个数组和一个标志位组成。数组中按照从小到大的顺序存储了所有整数值,标志位用于记录数组中存储的整数类型(即8位整数、16位整数还是32位整数)。如果标志位为0,则表示数组中存储的都是8位整数;如果标志位为1,则表示数组中存储的都是16位整数;如果标志位为2,则表示数组中存储的都是32位整数。
在IntSet中,每个整数值只会出现一次,重复的整数会被自动去重。在插入新的整数值时,IntSet会自动根据当前数组中存储的整数类型进行扩容,以保证能够存储新的整数值。
IntSet支持的操作包括插入、删除、查找、遍历等。具体来说,插入和删除操作的时间复杂度均为O(1);查找操作的时间复杂度为O(N),其中N为IntSet中存储的整数数量;遍历操作的时间复杂度为O(N)。
在Redis6中,IntSet进行了一些优化。具体来说,Redis6中的IntSet支持了快速的非对称迭代操作,这意味着可以在不需要完全遍历整个IntSet的情况下,快速地获取满足特定条件的整数值。此外,Redis6中的IntSet还支持了压缩操作,可以通过压缩来减少IntSet占用的内存空间。
在使用Redis时需要存储大量的整数值,可以考虑使用IntSet来优化存储空间和操作性能。在Redis6中,IntSet还支持了更多的优化和功能,可以更好地满足实际需求。
首先我们了解一下整数集的编码方式有三种:
INTSET_ENC_INT16:表示整数集中的元素都是16位的整数。
INTSET_ENC_INT32:表示整数集中的元素都是32位的整数。
INTSET_ENC_INT64:表示整数集中的元素都是64位的整数。
整数集的编码方式是根据元素的大小来自动选择的。如果所有元素都可以放在16位或32位中,就选择相应的编码方式;否则,就选择64位编码方式。
整数集的压缩算法是在保证元素按照升序排列的前提下,尽量压缩每个元素的存储空间。具体来说,整数集会对连续的整型数据进行压缩,只存储它们的起始值和步长,而不是每个元素的实际值。这种算法可以在尽可能少的内存空间中存储大量的整型数据,提高了内存的利用率。
先不看源码,我们先看八股文。
整数集(IntSet)的压缩算法是一种特殊的算法,可以在尽可能少的内存空间中存储大量的整型数据。整数集的压缩算法基于以下两个原则:
整数集中的元素按照升序排列。
对于连续的整型数据,只存储它们的起始值和步长,而不是每个元素的实际值。
具体来说,整数集会根据元素的大小选择合适的编码方式(INTSET_ENC_INT16、INTSET_ENC_INT32或INTSET_ENC_INT64),然后将整数集中的元素按照升序排列。对于连续的整型数据,整数集会计算它们的起始值和步长,并将它们存储在数据块中。例如,假设整数集中有以下元素:1、2、3、4、5、10、11、12、13、14,整数集的数据块可以存储以下内容:
+--------+--------+--------+--------+--------+--------+--------+--------+
| 1 | 1 | 4 | 10 | 1 | 5 | 2 | 4 |
+--------+--------+--------+--------+--------+--------+--------+--------+
在上面的数据块中,第一个元素1表示整数集的编码方式(INTSET_ENC_INT16),第二个元素1表示整数集中有6个元素,后面的数据块中,每两个元素表示一个连续的整型数据的起始值和步长。例如,第三个元素4表示整数集中有4个连续的整数(4、5、6、7),第四个元素10表示这些整数的起始值,第五个元素1表示这些整数的步长。
整数集的压缩算法可以在尽可能少的内存空间中存储大量的整型数据,提高了内存的利用率。在实际的Redis应用中,整数集被广泛应用于集合等数据结构的实现。通过使用整数集,Redis可以在保证高效的操作性能的同时,减少内存的浪费,提高内存利用率。
虽然在Redis中,整数集(IntSet)的编码方式是根据元素的大小来自动选择的。整数集的编码方式有三种:INTSET_ENC_INT16、INTSET_ENC_INT32和INTSET_ENC_INT64,分别表示整数集中的元素都是16位、32位和64位的整数。
当向整数集中添加元素时,Redis会根据新元素的大小和整数集中已有元素的大小,自动选择合适的编码方式。具体来说,如果新元素的大小可以放在整数集的当前编码方式中,就直接将新元素添加到整数集中;否则,Redis会根据新元素的大小和已有元素的大小,选择一个更大的编码方式,并将整数集中的所有元素转换为新的编码方式,然后再将新元素添加到整数集中。
例如,假设整数集中已有32位整数,此时向整数集中添加一个16位整数。由于16位整数可以放在32位整数中,因此Redis会直接将新元素添加到整数集中,不需要进行编码方式的转换。但是,如果向上面的整数集中添加一个64位整数,由于64位整数无法放在32位整数中,Redis会选择64位编码方式,并将整数集中的所有元素转换为64位编码方式,然后再将新元素添加到整数集中。
整数集的自动编码方式选择可以在保证高效性能的同时,减少内存的浪费,提高内存利用率。在实际的Redis应用中,整数集被广泛应用于集合等数据结构的实现。通过使用整数集,Redis可以在保证高效的操作性能的同时,减少内存的浪费,提高内存利用率。
说了在多理论的概念和举例都略显单薄,很多同学可能在想"talk is cheap, show me code"
。那么接下来我们进行一下源码解读。
在Redis的GitHub仓库中,IntSet的代码文件位于以下路径:
intset.h:https://github.com/redis/redis/blob/6.0/src/intset.h
intset.c:https://github.com/redis/redis/blob/6.0/src/intset.c
6.0分支是Redis6的分支,包含了最新的代码修改和功能更新。所以如果有同学想详细的了解一下,也可以在该分支下找到最新的IntSet代码文件,如果对Redis7的源码也想了解,只需要在上面切换一下分支即可。参考代码实现来深入了解IntSet的数据结构和操作还是很简单的。其实相比较前几个章节学习的底层数据结构,intset 不算复杂的。
注:如果你对C也算是了解的话,可能下面的内容就不必再看了,点击我上面的源码地址,扫一遍这两个文件基本上就一目了然。
如果你是C小白,请继续
。
上面截图中
intset.h
:IntSet的头文件,定义了IntSet的结构体类型和相关函数的声明。
intset.c
:IntSet源文件,实现了IntSet的各种操作函数,包括创建、添加、删除、查找和升级等操作函数。
从上面的源码截图我们可以看出来IntSet的定义如下:
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
IntSet是一个结构体类型,其中包含三个成员变量:
接下来,我们来看一下IntSet的基本操作函数。
intsetNew函数用于创建一个新的IntSet结构体,并返回该结构体的指针。函数定义如下:
intset *intsetNew(void) {
intset *is = zmalloc(sizeof(intset));
is->encoding = INTSET_ENC_INT16;
is->length = 0;
return is;
}
在该函数中,使用zmalloc函数动态分配了一段大小为sizeof(intset)的内存空间,并将其初始化为0。然后将该内存空间强制转换为intset类型,并将encoding设置为INTSET_ENC_INT16,表示使用16位整数编码方式。最后返回该IntSet结构体的指针。
intsetAdd函数用于向IntSet中添加一个整数值,函数定义如下:
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
uint8_t valenc = _intsetValueEncoding(value);
if (valenc > is->encoding) {
return intsetUpgradeAndAdd(is,value,success);
}
......
}
在intsetAdd
中首先调用_intsetValueEncoding
函数判断value的编码方式,并将其赋值给valenc。然后判断valenc是否大于当前IntSet的编码方式,如果是,则调用intsetUpgradeAndAdd
函数进行升级操作。否则,继续执行后面的操作。
接下来,根据valenc的值,将value插入到IntSet中。具体来说,如果valenc等于8,则将value转换为8位整数,并插入到IntSet的数组中;如果valenc等于16,则将value转换为16位整数,并插入到IntSet的数组中;如果valenc等于32,则将value转换为32位整数,并插入到IntSet的数组中。插入操作完成后,将success设置为1,表示插入成功。
intsetRemove
intsetRemove
函数用于从IntSet中删除一个整数值,函数定义如下:
int intsetRemove(intset *is, int64_t value, int *success) {
uint8_t valenc = _intsetValueEncoding(value);
uint8_t *p;
uint64_t u64;
int32_t i32;
int16_t i16;
uint8_t i8;
if (valenc <= is->encoding) {
......
}
*success = 0;
return 0;
}
在该函数中,首先调用_intsetValueEncoding
函数判断value的编码方式,并将其赋值给valenc。然后判断valenc是否小于等于当前IntSet的编码方式,如果是,则继续执行后面的操作。否则,直接返回0,表示删除失败。
接下来,根据valenc的值,在IntSet的数组中查找value,并将其删除。具体来说,如果valenc等于8,则将value转换为8位整数,并在数组中查找并删除;如果valenc等于16,则将value转换为16位整数,并在数组中查找并删除;如果valenc等于32,则将value转换为32位整数,并在数组中查找并删除。删除操作完成后,将success设置为1,表示删除成功。
intsetFind
函数用于在IntSet中查找一个整数值,函数定义如下:
uint8_t intsetFind(intset *is, int64_t value) {
uint8_t valenc = _intsetValueEncoding(value);
if (valenc <= is->encoding) {
......
}
return 0;
}
在该函数中,首先调用_intsetValueEncoding函数判断value的编码方式,并将其赋值给valenc。然后判断valenc是否小于等于当前IntSet的编码方式,如果是,则继续执行后面的操作。否则,直接返回0,表示查找失败。
接下来,根据valenc的值,在IntSet的数组中查找value。具体来说,如果valenc等于8,则将value转换为8位整数,并在数组中查找;如果valenc等于16,则将value转换为16位整数,并在数组中查找;如果valenc等于32,则将value转换为32位整数,并在数组中查找。如果在数组中找到了value,则返回1,表示查找成功;否则返回0,表示查找失败。
intsetUpgradeAndAdd
函数用于将IntSet的编码方式升级,并向IntSet中添加一个整数值,函数定义如下:
/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value, uint8_t *success) {
uint8_t curenc = is->encoding;
uint8_t newenc = _intsetValueEncoding(value);
int length = is->length;
int prepend = value < 0 ? 1 : 0;
is->encoding = newenc;
while(length--) {
int64_t v;
_intsetGet(is,length,&v);
intsetRemove(is,v,NULL);
intsetAdd(is,v,success);
}
*success = intsetAdd(is,value,success);
return is;
}
在该函数中,首先获取当前IntSet的编码方式curenc、新的整数值value的编码方式newenc和IntSet的长度length。然后根据value的正负性,判断是否需要在数组的开头添加新的整数值。
接下来,将IntSet的编码方式设置为newenc,并遍历IntSet中的每个整数值。对于每个整数值,先在IntSet中删除,然后重新添加到IntSet中,这样可以将所有整数值的编码方式都升级到newenc。
最后,向IntSet中添加新的整数值value,并将success设置为1,表示添加成功。
通过扫一遍源码我们基本上可以了解到IntSet提供了多种操作函数,包括添加整数、删除整数、查找整数、升级整数集合的操作等。了解到这些操作函数的实现原理,包括如何进行二分查找、如何调整整数集合的大小等。它可以在O(log N)的时间复杂度内完成查找、添加、删除操作等。如何利用连续内存和二分查找来提高性能等。
在实际的Redis应用中,整数集被广泛应用于集合等数据结构的实现。通过使用整数集,Redis可以在保证高效的操作性能的同时,减少内存的浪费,提高内存利用率。可以用于存储大量的整数值,并支持快速的插入、删除和查找操作。如果您需要存储大量的整数值,可以考虑使用IntSet来优化存储空间和操作性能。
应一位网友的建议,在每个章节后面留个思考题,供大家继续学习。下次分享来揭晓答案。本次的思考题只有一道。
1. Redis6是如何使用IntSet来实现集合和有序集合?
《Redis从入门到精通【高阶篇】之底层数据结构字典(Dictionary)详解》
《Redis从入门到精通【高阶篇】之底层数据结构快表QuickList详解》
《Redis从入门到精通【高阶篇】之底层数据结构简单动态字符串(SDS)详解》
《Redis从入门到精通【高阶篇】之底层数据结构压缩列表(ZipList)详解》
《Redis从入门到精通【进阶篇】之数据类型Stream详解和使用示例》
大家好,我是冰点,今天的Redis从入门到精通【高阶篇】之底层数据结构整数集(IntSet)详解,全部内容就是这些。如果你有疑问或见解可以在评论区留言。