Redis中的Set与Java中的HashSet一样,无序且存储元素不重复。
Redis的集合对象Set使用了intset和hashtable两种数据结构存储。intset我们可以理解为数组,hashtable就是普通的哈希表(key为Set集合中元素的值,value为null)。当value是整数值时,且数据量不大时使用inset来存储,其他情况都是用字典dict来存储。
比如我有1个Set,元素为ABC。在hashtable中对应就是3个entry,key是ABC,value是null。
编码转换
Set的底层存储intset和hashtable是存在编码转换的,使用intset存储必须满足下面两个条件,否则使用hashtable,条件如下:
- 集合对象保存的所有元素都是整数值
intset集合对象保存的元素数量不超过512个
intset内部其实是一个数组(int8_t coentents[]数组),而且存储数据的时候是有序的,因为在查找数据的时候是通过二分查找来实现的。
intset:当集合中的元素都是整数时,Redis会采用intset编码方式存储。intset编码方式的优点是存储空间小,操作效率高。
hashtable:当集合中的元素包含字符串时,Redis会采用hashtable编码方式存储。hashtable编码方式的优点是可以存储任意类型的元素,支持字符串操作。缺点是存储空间相对较大,操作效率相对较低。
添加过程
以set的sadd命令为例子,整个添加过程如下:
- 检查set是否存在不存在则创建一个set结合。
- 根据传入的set集合一个个进行添加,添加的时候需要进行内存压缩。
setTypeAdd执行set添加过程中会判断是否进行编码转换。
稍微深入分析一下set的单个元素的添加过程,首先如果已经是hashtable的编码,那么我们就走正常的hashtable的元素添加,如果原来是intset的情况,那么我们就需要进行如下判断:
如果能够转成int的对象(isObjectRepresentableAsLongLong),那么就用intset保存。
- 如果用intset保存的时候,如果长度超过512就转为hashtable编码。
- 其他情况统一用hashtable进行存储。
整数数组介绍
intset,也就是整数集合,是 set 的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用 intset 作为 set 的底层实现。
它的查找是 O(log n) 的,插入和删除都是 O(n) 的。但是由于存储元素相对较少的时候,O(log n) 和 O(n) 差距不是很大,但是用 Redis 的这种 intset,相比红黑树和哈希表来说,可以大大减少内存。
所以,Redis 的 整数集合 intset 的存在主要还是为了节省内存。
整数数组结构
整数集合可用保存的数据类型有:int16t 、int32t 和 int64_t 的整数值,并且保证集合中不会出现重复元素。
整数集合定义如下:
``` // src/intset.h typedef struct intset { uint32t encoding; // 编码方式,后面会详细解释 uint32t length; // 集合中元素的个数,也就是contents数组的长度 int8t contents[]; // 保存元素的数组 } intset; /* Note that these encodings are ordered, so: INTSETENCINT16 < INTSETENCINT32 < INTSETENC_INT64. */
define INTSETENCINT16 (sizeof(int16_t))
define INTSETENCINT32 (sizeof(int32_t))
define INTSETENCINT64 (sizeof(int64_t))
```
分析: contents数组是整数集合的底层实现:整数集合中的每一个元素就是contents数组中的一个元素,每个元素在数组中按照从小到大的顺序排列,没有重复元素。
虽然contents数组被声明为 int8t 类型的数组,但实际上contents数组并不保存任何int8t 类型的值。
contents数组实际存储的类型取决于encoding的值。
encoding的取值可以是:INTSETENCINT16、INTSETENCINT32 或 INTSETENCINT64,每种编码的取值范围如下:
INTSETENCINT16 取值范围:[−2^15 ,2^15−1] 即:[-32768, 32767 ]
INTSETENCINT32 取值范围:[−2^31 ,2^31−1] 即:[-2147483648, 2147483647]
INTSETENCINT64 取值范围:[−2^63 ,2^63−1] 即:[-9223372036854775808, 9223372036854775807]
整数数组升级
每当我们要将一个新元素添加到 intset 里面,并且新元素的类型比 intset 现有所有元素的类型都要长时,intset 需要先进行升级(upgrade),然后才能将新元素添加到整数集合里面。
升级 intset 并添加新元素主要分为以下三步进行:
1、根据新元素的类型,扩展 intset 底层数组的空间大小,并为新元素分配空间。(分配空间)
2、将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性性质不变。(调整位置类型)
3、将新元素添加到底层数组里面(添加元素)。
注:intset 添加新元素的时间复杂度为O(N)。
假设有个编码为INTSETENCINT16 的整数数组如下所示:
每次往这个整数集合中插入新元素时,会检查新元素的类型,是不是比当前contents数组的类型长,如果是,整数集合需要先进行升级操作。比如:现在我们要把编码为int32_t的元素插入到整数集合里,因为65535的类型比集合中当前元素类型要长,所以要先对整数集合升级。
升级的好处
灵活:C语言是静态类型的语言,为了避免类型错误,我们通常是一种类型就放到一种类型的数据结构里,如:我们一般会使用int16t类型的数组保存int16t类型的值,用int32t类型数组保存int32t类型的值。而整数集合允许随意的将不同类型的整型值添加到集合中,而不用担心类型错误。
节约内存:如果想让一个数组同时保存,int16t、int32t和int64t的值,最简单的方式就是使用int64t类型的数组,但是这会造成空间的浪费。inset只会在适当的时候才会触发升级操作,其他时间都是尽可能节约内存。
整数数组降级
intset 不支持降级操作, 一旦对数组进行了升级,编码就会一直保持升级后的状态。
为什么不实现降级?
1、加入元素超过当前长度我们很容易就知道此时需要进行升级操作,但是当我们删除一个数据时我们如何判断是否需要降级却很困难,我们需要重新遍历一遍剩下的元素是否小于当前长度,实现复杂度 O(N) 。这就是为什么不进行降级原因之一。
2、你可能会说重新遍历一遍很快的,反正在内存中,那么你有没有想过如果降级之后又遇到升级情况,这样来回的升级降级就降低了我们程序的性能了。我们知道升级是必须的所以这里降级 Redis 采取的是忽略的策略。
参考链接: https://blog.csdn.net/weixin_46935110/article/details/127898048
参考链接:https://blog.csdn.net/weixin_43871678/article/details/119488486
参考链接:https://developer.aliyun.com/article/1254079