Redis源码阅读 - 整数集合的设计与实现

Redis中set结构类似于数学上的集合的概念,它包含的元素无序,且不能重复。Redis里的set结构还实现了基础的集合并、交、差的操作。与Redis对外暴露的其它数据结构类似,set的底层实现,随着元素类型是否是整型以及添加的元素的数目多少,而有所变化。

概括来讲,当set中添加的元素都是整型且元素数目较少时,set使用intset作为底层数据结构,否则,set使用dict作为底层数据结构。

在之前哈希表的介绍中,我们已经了解过字典的实现。今天我们只介绍intset数据结构,它的定义在intset.h/intset.c中:

 

intset顾名思义,是由整数组成的集合。实际上,intset是一个由整数组成的有序集合,从而便于在上面进行二分查找,用于快速地判断一个元素是否属于这个集合。它在内存分配上与ziplist有些类似,是连续的一整块内存空间,而且对于大整数和小整数(按绝对值)采取了不同的编码,尽量对内存的使用进行了优化。

各个字段含义如下:

  • encoding: 数据编码,表示intset中的每个数据元素用几个字节来存储。

  • length: 表示intset中的元素个数。encoding和length两个字段构成了intset的头部(header)。

  • contents: 是一个柔性数组(flexible array member)表示intset的header后面紧跟着数据元素。这个数组的总长度(即总字节数)等于encoding * length。柔性数组在Redis的很多数据结构的定义中都出现过(例如sds, quicklist, skiplist),用于表达一个偏移量。contents需要单独为其分配空间,这部分内存不包含在intset结构当中。

需要注意的是intset中的编码,它有三种可能的取值:INTSET_ENC_INT16表示每个元素用2个字节存储,INTSET_ENC_INT32表示每个元素用4个字节存储,INTSET_ENC_INT64表示每个元素用8个字节存储。因此,intset中存储的整数最多只能占用64bit。

 

我们知道,intset可能会随着数据的添加而改变它的数据编码:最开始,新创建的intset使用占内存最小的INTSET_ENC_INT16(值为2)作为数据编码。每添加一个新元素,则根据元素大小决定是否对数据编码进行升级。

下面我们看下intset相关的方法,用来实现集合的创建、元素添加、删除、查找等等:

 

从上面的介绍我们知道,inset内部是一个柔性、有序、整数、数组。因此intsetFind方法,是一个典型的二分查找算法,来判断一个元素是否在集合中。我们来详细看下这个方法的实现:

 

从以上代码我们可以看到:

  • intsetFind在指定的intset中查找指定的元素value,找到返回1,没找到返回0。

  • _intsetValueEncoding函数会根据要查找的value落在哪个范围而计算出相应的数据编码(即它应该用几个字节来存储)。

  • 如果value所需的数据编码比当前intset的编码要大,则它肯定在当前intset所能存储的数据范围之外(特别大或特别小),所以这时会直接返回0;否则调用intsetSearch执行一个二分查找算法。

  • intsetSearch在指定的intset中查找指定的元素value,如果找到,则返回1并且将参数pos指向找到的元素位置;如果没找到,则返回0并且将参数pos指向能插入该元素的位置。

下面我们看下核心函数intsetSearch的实现:

 

我们知道intsetSearch是对于二分查找算法的一个实现,它大致分为三个部分:

  • 特殊处理intset为空的情况。

  • 特殊处理两个边界情况:当要查找的value比最后一个元素还要大或者比第一个元素还要小的时候。实际上,这两部分的特殊处理,在二分查找中并不是必须的,但它们在这里提供了特殊情况下快速失败的可能。

  • 真正执行二分查找过程。注意:如果最后没找到,插入位置在min指定的位置。

代码中出现的intrev32ifbe是为了在需要的时候做大小端转换的。前面我们提到过,intset里的数据是按小端(little endian)模式存储的,因此在大端(big endian)机器上运行时,这里的intrev32ifbe会做相应的转换。这个查找算法的总的时间复杂度为O(log n)。

自此我们对整数集合有了一个整体上的认识。另外需要注意的是有以下几种情况可会让intset转成dict结构:

  • 由于添加非数字元素造成集合底层由intset转成dict

  • 添加了一个数字,但它无法用64bit的有符号数来表达。intset能够表达的最大的整数范围为-264~264-1,因此,如果添加的数字超出了这个范围,这也会导致intset转成dict。

  • 添加的集合元素个数超过了set-max-intset-entries配置的值的时候,也会导致intset转成dict(具体的触发条件参见t_set.c中的setTypeAdd相关代码)。

对于小集合使用intset来存储,主要的原因是节省内存。特别是当存储的元素个数较少的时候,字典所带来的内存开销要大得多(包含两个哈希表、链表指针以及大量的其它元数据)。所以,当存储大量的小集合而且集合元素都是数字的时候,用intset能节省下一笔可观的内存空间。

实际上,从时间复杂度上比较,intset的平均情况是没有dict性能高的。以查找为例,intset是O(log n)的,而dict可以认为是O(1)的。但是,由于使用intset的时候集合元素个数比较少,所以这个影响并不是很大。

更多关于集合 交、差、并 算法的实现代码,在t_set.c中,有兴趣可以详细阅读,了解作者在处理各种问题的取舍,算法实现的细节,还是会有很多收获的。

欢迎扫码,关注我的微信公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值