Redis底层数据结构——IntSet

redis底层数据结构
Redis底层数据结构——SDS



一、IntSet介绍

Redis 中的 intset 是一个只存储整数的有序集合。intset是redis数据结构Set的底层实现之一,当Set中存放的元素全是整数时,redis会选择intset来存储。如果此时往Set中插入一个字符串,此时Set的存储方式会切换为hashtable

二、IntSet源码分析

1. intset结构体

// intset.h
typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

encoding:intset的内部编码,决定了contents中的整数采用16位、32位还是64位存储
length:contents中元素的个数
contents:灵活数组,存放真实数据,类型可以是int16_t、int32_t、int64_t

encoding的宏定义如下:

// sds.c
#define INTSET_ENC_INT16 (sizeof(int16_t)) // contents中的每个元素用 2 byte 来存储
#define INTSET_ENC_INT32 (sizeof(int32_t)) // contents中的每个元素用 4 byte 来存储
#define INTSET_ENC_INT64 (sizeof(int64_t)) // contents中的每个元素用 8 byte 来存储

虽然length的最大值为2^32-1,但redis对intset能存储的元素个数限制在了512个。可以通过config get set-max-intset-entries查看。

个人在刚学习时有这样的疑问:

既然encoding类型只有3种,length最大长度也被限制在了512,为啥不将encoding和length类型设置为uint8_t?这样就能节省三倍的空间,就算考虑到将来要扩展,那为什么不设置为unit16_t?2^16-1个元素足够应付绝大多数场景。查了一圈后都说这跟内存对齐有关,uint32_t刚好满足4字节对齐。那为什么sds还存在lenalloc都是uint8_t类型的sdshdr8呢,这时候又不考虑内存对齐了吗?

2. intset升级

当向intset中添加一个与原数据编码方式不同的整数时,intset需要进行升级。例如,如果intset中存放的是1,2,3,此时插入一个100000,由于1,2,3都是用int16_t来存储的,而100000超过了2^15-1,因此需要将intset中的1,2,3的int16_t编码方式升级到int32_t。

/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    // 获取当前intset的编码
    uint8_t curenc = intrev32ifbe(is->encoding);
    // 获取新插入值的编码,虽然传入的value类型为int64_t,但_intsetValueEncoding函数还是会对value进行判断
    uint8_t newenc = _intsetValueEncoding(value);
    // 获取元素个数
    int length = intrev32ifbe(is->length);
    // 判断新插入值是大于0还是小于0
    int prepend = value < 0 ? 1 : 0;

    /* First set new encoding and resize */
    // 将当前intset的编码设置为新插入值的编码
    is->encoding = intrev32ifbe(newenc);
    // 重置数组大小
    is = intsetResize(is,intrev32ifbe(is->length)+1);

    /* Upgrade back-to-front so we don't overwrite values.
     * Note that the "prepend" variable is used to make sure we have an empty
     * space at either the beginning or the end of the intset. */
    // 倒序遍历,逐个搬运元素到新的位置,_intsetGetEncoded按照旧编码方式查找旧元素
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    /* Set the value at the beginning or the end. */
    // 根据正负插入新元素
    if (prepend)
        _intsetSet(is,0,value); // 往队首插入
    else
        _intsetSet(is,intrev32ifbe(is->length),value); // 往队尾插入
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

需要注意的是,在搬运原来的元素到新的位置时,需要倒序。例如,数组中的元素从2个字节变成了4个字节,内存地址发生了改变。如果正序,则前面的数据会覆盖掉后面的数据。

3. intset元素查找

static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
    int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
    int64_t cur = -1;

    /* The value can never be found when the set is empty */
    if (intrev32ifbe(is->length) == 0) {
        if (pos) *pos = 0;
        return 0;
    } else {
        /* Check for the case where we know we cannot find the value,
         * but do know the insert position. */
        // 如果待查找的值大于intset的最大值 或者 小于最小值,则表示没查找到
        if (value > _intsetGet(is,max)) {
            if (pos) *pos = intrev32ifbe(is->length);
            return 0;
        } else if (value < _intsetGet(is,0)) {
            if (pos) *pos = 0;
            return 0;
        }
    }
    // 二分查找
    while(max >= min) {
        mid = ((unsigned int)min + (unsigned int)max) >> 1;
        cur = _intsetGet(is,mid);
        if (value > cur) {
            min = mid+1;
        } else if (value < cur) {
            max = mid-1;
        } else {
            break;
        }
    }

    // 如果待查找的值等于cur,则找到,否则没找到
    if (value == cur) {
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
}

时间复杂度:O(logN)

4. intset元素插入

//is:待插入的intset,value:要插入的值,succes:是否插入成功
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    // 获取新插入值的编码
    uint8_t valenc = _intsetValueEncoding(value);
    // 要插入的位置
    uint32_t pos;
    if (success) *success = 1;

    /* Upgrade encoding if necessary. If we need to upgrade, we know that
     * this value should be either appended (if > 0) or prepended (if < 0),
     * because it lies outside the range of existing values. */
    // 判断新插入值是否超过了当前intset的编码
    if (valenc > intrev32ifbe(is->encoding)) {
        /* This always succeeds, so we don't need to curry *success. */
        //超出编码范围,需要升级
        return intsetUpgradeAndAdd(is,value);
    } else {
        /* Abort if the value is already present in the set.
         * This call will populate "pos" with the right position to insert
         * the value when it cannot be found. */
        // 在iniset中查找value,如果找到,可以不用插入新元素,则返回找到的位置pos;
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }
        // 如果没找到,则需要对intset扩容
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        // 移动数组中pos之后的元素到pos+1,给新元素腾出空间
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    // 插入新元素
    _intsetSet(is,pos,value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

时间复杂度:

  • 如果触发升级,则需要将原数组中的元素全部往后移动,此时时间复杂度为O(N)
  • 如果没触发升级,需要执行二分查找,此时时间复杂度为O(logN)

5. intset元素删除

intset *intsetRemove(intset *is, int64_t value, int *success) {
    // 获取待删除整数的编码
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    if (success) *success = 0;
    // 如果待删除整数的编码小于等于当前intset的编码 且 存在于intset中
    if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
        uint32_t len = intrev32ifbe(is->length);

        /* We know we can delete */
        if (success) *success = 1;

        /* Overwrite value with tail and update length */
        if (pos < (len-1)) intsetMoveTail(is,pos+1,pos); //将后面的数据往前移动一个位置
        is = intsetResize(is,len-1); //重新给intset分配内存
        is->length = intrev32ifbe(len-1); //设置长度-1
    }
    return is;
}

时间复杂度:

由于要执行二分查找,考虑最坏情况,删除intset第一个元素,需要将后面的元素全部往前移动,此时时间复杂度为O(N)

三、一些函数和自定义宏

1. 自定义宏

// endianconv.h
#if (BYTE_ORDER == LITTLE_ENDIAN)
#define memrev16ifbe(p) ((void)(0))
#define memrev32ifbe(p) ((void)(0))
#define memrev64ifbe(p) ((void)(0))
#define intrev16ifbe(v) (v)
#define intrev32ifbe(v) (v)
#define intrev64ifbe(v) (v)
#else
#define memrev16ifbe(p) memrev16(p)
#define memrev32ifbe(p) memrev32(p)
#define memrev64ifbe(p) memrev64(p)
#define intrev16ifbe(v) intrev16(v)
#define intrev32ifbe(v) intrev32(v)
#define intrev64ifbe(v) intrev64(v)
#endif

如果机器是小端字节序,则不做任何操作;如果是大端字节序,需要进行数据翻转。

2. 函数

2.1 _intsetValueEncoding

//给定一个整数,返回编码格式,例如INTSET_ENC_INT16 
static uint8_t _intsetValueEncoding(int64_t v)

2.2 intsetResize

//给定原intset和元素个数,给intset分配 len*encoding大小的内存,返回intset指针
static intset *intsetResize(intset *is, uint32_t len) 

2.3 _intsetGetEncoded

//根据encoding,将intset中pos位置处的元素拷贝到一个临时变量中,并返回这个临时变量。该函数用于intset升级时拷贝原数据
static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc)

2.4 intsetMoveTail

//将intset中from位置处的元素移到to位置处
static void intsetMoveTail(intset *is, uint32_t from, uint32_t to)

2.5 字节序翻转

void memrev16(void *p) {
    unsigned char *x = p, t;

    t = x[0];
    x[0] = x[1];
    x[1] = t;
}

uint16_t intrev16(uint16_t v) {
    memrev16(&v);
    return v;
}
### Redis 数据结构底层实现详解 #### 字符串类型 (String) Redis 的字符串能够存储多种类型的数据,包括但不限于二进制数据、文本数据以及数值型数据。对于这种类型的对象,在设计上采用了简单动态字符串(SDS, Simple Dynamic String),以确保高效的操作与管理[^3]。 ```c struct sdshdr { int len; /* 已使用的字节数 */ int free; /* 剩余可用空间数 */ char buf[]; /* 存储实际字符数组 */ } ``` 通过这种方式,不仅实现了对字符串长度的有效追踪,同时也简化了追加操作并减少了频繁分配/释放带来的开销。 #### 列表类型 (List) 列表键的底层可以通过两种不同的形式来表示:当元素数量较少且单个元素较短时采用压缩列表;反之,则会转为双向链表的形式。前者旨在优化小型集合场景下的资源利用率,后者则更适用于处理较大规模或复杂度较高的情况[^4]。 ```c // 双向链表节点定义 typedef struct listNode { void *value; struct listNode *prev; struct listNode *next; } listNode; // 压缩列表项头文件部分展示 typedef struct zlentry { unsigned int prevrawlensize; unsigned int prevrawlen; unsigned int lensize; unsigned int len; // ...其他成员... } zlentry; ``` #### 集合类型 (Set) 为了提供高效的去重功能,集合通常基于整数集或是哈希表这两种结构之一构建而成。具体选择取决于所含元素的数量及其特性——少量的小范围整数适合用紧凑的方式保存,而一般情形下则利用哈希映射达到快速查找的目的。 #### 散列类型 (Hash) 散列表同样存在两种可能的内部表现形态:如果字段名和值都比较简短的话,那么就会被封装在一个专门定制化的ziplist里边;否则便会被拆分成多个独立的对象,并借助hashtable来进行关联维护。 #### 有序集合类型 (Sorted Set) 该类别的核心在于维持一个按分数排序的成员序列。为此目的,选择了跳跃表作为主要支撑技术,允许O(logN)级别的插入删除效率的同时还便于迭代访问整个区间内的条目。 综上所述,通过对不同应用场景需求的理解和技术手段的选择,使得每一种基础组件都能发挥出最佳效能,从而构成了Redis强大而又灵活的数据管理系统。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值