Redis数据结构之整数集合(intset)

整数集合

整数集合的概念

整数集合是redis用来保存整数值的集合的一种数据结构,可以用来保存int类型数据,并且可以保证不会出现重复元素。因此当一个集合中只包含整数元素且数量不多的时候,redis会选择使用整数集合作为底层实现。

整数集合在redis源码中的定义如下

typedef struct intset {
	// 编码方式
    uint32_t encoding;
    // 集合的长度
    uint32_t length;
    // 保存整数元素的数组
    int8_t contents[];
} intset;

contents是整数集合的底层实现,保存了整数集合的每一个元素,每个元素在该数组中从小到大有序排列,并且不重复(如何保证有序性和唯一性我们后面讨论插入的时候在说)。contens数组虽然声明为int8_t类型,但其实真正的类型取决于encoding的值。在操作一个整数集合的时候,会首先获取encoding的值。代码如下

#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

/* 返回适用于所提供的值得编码方式 */
static uint8_t _intsetValueEncoding(int64_t v) {
    if (v < INT32_MIN || v > INT32_MAX)
        return INTSET_ENC_INT64;
    else if (v < INT16_MIN || v > INT16_MAX)
        return INTSET_ENC_INT32;
    else
        return INTSET_ENC_INT16;
}
数据类型类型字节数最小值最大值
int8_tchar1-128127
int16_tshort2-32,76832,767
int32_tint4-2,147,483,648-2,147,483,648
int64_tlong8-9,223,372,036,854,775,8089,223,372,036,854,775,807

整数集合的升级

假设我们有一个整数集合,他现在存储的都是int16_t类型的整数,而现在我们需要向其中添加一个int32_t类型的整数。这个时候因为C语言是静态性语言,我们不能将两种不同类型的值放在同一个数据结构中,这样会带来类型错误,所以我们就需要将原有的数据结构做相应的改变,让他能存储更大的整数,同时将原有的整数类型也改变为更大的类型,这个过程就是整数集合的升级(upgrade)。代码如下:

// intset.c

/* 升级整数集合,让他能存放更大的数 */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {

	// 获取当前的编码方式,intrev32ifbe是大小端转换函数
    uint8_t curenc = intrev32ifbe(is->encoding);
    
    // 插入的新值需要的编码方式
    uint8_t newenc = _intsetValueEncoding(value);
    
    // 当前整数集合的长度
    int length = intrev32ifbe(is->length);
    
	// 前置标志,代码能到这一步,证明升级是有必要的,即value的值超出了原有的范围,所以他一定是要么在最后面,要么小于0,在最前面
    int prepend = value < 0 ? 1 : 0;

    // 设置原来整数集合的编码方式为最新值
    is->encoding = intrev32ifbe(newenc);

	// 调整原来的整数集合,主要为根据新的长度计算出升级需要的内从空间,然后修改原来整数集合分配到的内存块的大小
    is = intsetResize(is,intrev32ifbe(is->length)+1);
	
    // 从后往前升级,保证原来的值不会被覆盖
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    // 如果新值value<0,就插入到第一个位置,否则插入到最后一个位置
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    
    // 更新整数集合的长度
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}
static void _intsetSet(intset *is, int pos, int64_t value) {
    uint32_t encoding = intrev32ifbe(is->encoding);

    if (encoding == INTSET_ENC_INT64) {
        ((int64_t*)is->contents)[pos] = value;
        memrev64ifbe(((int64_t*)is->contents)+pos);
    } else if (encoding == INTSET_ENC_INT32) {
        ((int32_t*)is->contents)[pos] = value;
        memrev32ifbe(((int32_t*)is->contents)+pos);
    } else {
        ((int16_t*)is->contents)[pos] = value;
        memrev16ifbe(((int16_t*)is->contents)+pos);
    }
}

升级的好处:

  1. 因为整数集合可以自动升级,所以我们可以随意的往里面存放int16_t、int32_t、int64_t等不同类型的整数,而不会发生类型错误。
  2. 节约内存,因为如果没有升级,我们要在一个数组里面存放不同类型的数,就只能在一开始把数组定义成int64_t类型的,而有时候我们其实只在里面存储了int16_t类型的,就造成了内存浪费。而有了升级功能以后,我们就可以在需要的时候在去扩展内存,节约了资源。整数集合只支持升级,无法降级。

插入操作

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
	
	// 获取value需要的编码方式
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    //是否插入成功的标志
    if (success) *success = 1;

    // 如果编码方式超出了原来的界限,就进行升级 
    if (valenc > intrev32ifbe(is->encoding)) {
        return intsetUpgradeAndAdd(is,value);
    } else {
    
        // 插入value前,先查找该值是否已经存在,如果存在,就设置插入成功的标志为失败,结束程序
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }
		
		// 调整整数集合,修改原来集合所用内存块大小,分配空间
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        // 确定value要插入的位置后,将原来在这个位置的元素统一向后移
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }

	// 把value插入到pos标记的位置
    _intsetSet(is,pos,value);
    // 更新整数集合的长度
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}
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;

    // 如果该整数集合是空的,那么设置pos为0,说明value可以插入到第一个位置,结束程序,返回0。
    if (intrev32ifbe(is->length) == 0) {
        if (pos) *pos = 0;
        return 0;
    } else {
    
        // 如果value大于集合中最后一个元素,那么也不用去查找,设置pos为集合的长度,说明value可以插入到最后一个位置。
        if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
            if (pos) *pos = intrev32ifbe(is->length);
            return 0;

		 // 如果value小于集合中第一个元素,那么也不用去查找,设置pos为0,说明value可以插入到第一个位置。
        } else if (value < _intsetGet(is,0)) {
            if (pos) *pos = 0;
            return 0;
        }
    }
	
	// 因为整数集合是有序的,所以这里使用二分查找来定位value的位置
    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;
        }
    }

	// 检查是否找到了value,设置pos的位置,结束程序。
    if (value == cur) {
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
}
static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {
    void *src, *dst;
    uint32_t bytes = intrev32ifbe(is->length)-from;
    uint32_t encoding = intrev32ifbe(is->encoding);

    if (encoding == INTSET_ENC_INT64) {
        src = (int64_t*)is->contents+from;
        dst = (int64_t*)is->contents+to;
        bytes *= sizeof(int64_t);
    } else if (encoding == INTSET_ENC_INT32) {
        src = (int32_t*)is->contents+from;
        dst = (int32_t*)is->contents+to;
        bytes *= sizeof(int32_t);
    } else {
        src = (int16_t*)is->contents+from;
        dst = (int16_t*)is->contents+to;
        bytes *= sizeof(int16_t);
    }
	//批量移动数据
    memmove(dst,src,bytes);
}

今天就到这儿了,剩下的闲了在续。

参考资料:《Redis设计与实现》 黄建宏

redis源码学习_整数集合

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半__夏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值