【redis数据结构】五分钟带你由浅入深学习IntSet

292 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情 >>

redis数据结构——IntSet

IntSet是Redis中set集合的一种实现方式,它是基于整数数组来实现,并且具备长度可变、有序等特征。

InSet的实现结构

下面我们一起来看看redis中是怎么定义IntSet的:

encoding其实是一个标志量,记录着当前IntSet的类型;length顾名思义就是整数数组contents[]的值了。

typedef struct intset {
    uint32_t encoding; /* 编码方式,支持存放16位、32位、64位整数*/
    uint32_t length; /* 元素个数 */
    int8_t contents[]; /* 整数数组,保存集合数据*/
} intset;

其中的encoding包含三种模式(编码方式,支持存放16位、32位、64位整数),表示存储的整数大小不同:

/* Note that these encodings are ordered, so:
 * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t)) /* 2字节整数,范围类似java的short*/
#define INTSET_ENC_INT32 (sizeof(int32_t)) /* 4字节整数,范围类似java的int */
#define INTSET_ENC_INT64 (sizeof(int64_t)) /* 8字节整数,范围类似java的long */

数组是连续存储的,redis中为了能够方便查找数据,会将IntSet中所有的整数按照升序依次保存在contents数组中,下面举个栗子说明一下:

比如我们要存的是1,2,3这三个数据,那对应IntSet中的结构,如下:

(可以看到在IntSet中1,2,3是按照升序排列的)

image.png

因为1,2,3每个数字都在int16_t的范围内,因此采用的编码方式是INTSET_ENC_INT16,每部分占用的字节大小如下:

  • encoding:4字节(采用16位编码)
  • length:4字节
  • contents:2字节 * 3 = 6字节

IntSet升级

假设我们现在有一个IntSet,里面的元素分别为{5,10,22},此时只需要采用 INTSET_ENC_INT16编码即可 则每个数字占用2个字节,结构如下: image.png

而这时候因为业务需要,突然来了一个数字(50000),很明显50000已经超过了int16_t的表示范围,此时IntSet会自动升级到合适大小的编码模式。

在这里需要特别注意一点:IntSet中的,encoding的16,32,64位所指的是有符号数的,比如INTSET_ENC_INT16编码模式下所能表示的范围是-32768~32767;50000明显是不在这个范围内的,所以IntSet会自动升级到INTSET_ENC_INT32编码模式。

下面我们先用文字描述一下大概的升级过程:

1、IntSet升级编码为INTSET_ENC_INT32, 每个整数占4字节,并按照新的编码方式及元素个数扩容数组

2、倒序依次将数组中的元素拷贝到扩容后的正确位置

3、将待添加的元素50000放入数组末尾

4、最后,将inset的encoding属性改为INTSET_ENC_INT32,将length属性改为4

在这里提个问题: 在上述步骤2中为什么是倒序拷贝的,顺序不可以吗?

答案是不可以的:

image.png 试想一下,如果不是倒序,而是顺序扩容的话,那率先扩容的就是5了,从2个字节扩容到4个字节,很明显这样扩容的话,元素5会破坏掉元素10的,其他以此类推。

下面是IntSet的编码升级过程的源码(在每一行中都已经给出了注释,可以很容易看懂):

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    // 获取当前intset编码
    uint8_t curenc = intrev32ifbe(is->encoding);
    // 获取新编码
    uint8_t newenc = _intsetValueEncoding(value);
    // 获取元素个数
    int length = intrev32ifbe(is->length); 
    // 判断新元素是大于0还是小于0 ,小于0插入队首、大于0插入队尾
    int prepend = value < 0 ? 1 : 0;
    // 重置编码为新编码
    is->encoding = intrev32ifbe(newenc);
    // 重置数组大小
    is = intsetResize(is,intrev32ifbe(is->length)+1);
    // 倒序遍历,逐个搬运元素到新的位置,_intsetGetEncoded按照旧编码方式查找旧元素
    while(length--) // _intsetSet按照新编码方式插入新元素
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
    /* 插入新元素,prepend决定是队首还是队尾*/
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    // 修改数组长度
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}