9.Redis数据结构之整数数组

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编码方式的优点是可以存储任意类型的元素,支持字符串操作。缺点是存储空间相对较大,操作效率相对较低。

image.png

添加过程

 以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 的整数数组如下所示:

image.png

每次往这个整数集合中插入新元素时,会检查新元素的类型,是不是比当前contents数组的类型长,如果是,整数集合需要先进行升级操作。比如:现在我们要把编码为int32_t的元素插入到整数集合里,因为65535的类型比集合中当前元素类型要长,所以要先对整数集合升级。

image.png

升级的好处

灵活: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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值