redis之intset

一、简介

redis包含了集合和有序集合。有序集合则是每个元素根据大小进行排序。redis中有序集合通过hash结构进行组织存储,当有序集合中所有元素都是整数时,使用hash则很浪费空间,所以当有序集合中都是整数时,使用intset。当在intset中加入了字符串元素,则整个intset将转化为hash结构。

  • 集合中每个元素都不重复
  • 有序集合,元素间有大小顺序

二、intset整体结构

请添加图片描述

三、实现

3.1 结构定义

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

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

3.2 创建空的intset

请添加图片描述

intset *intsetNew(void) {
    intset *is = zmalloc(sizeof(intset));
    is->encoding = intrev32ifbe(INTSET_ENC_INT16);
    is->length = 0;
    return is;
}

默认使用2字节(int61_t)的类型存储数据

3.3 例1.插入“10”

3.3.1 计算插入值的使用数据类型

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;
}

10 属于int16_t范围内,所以encoding = INTSET_ENC_INT16

3.3.2 判断intset当前编码是否满足新插入的值

  if (valenc > intrev32ifbe(is->encoding)) {
       ...
       //Upgrade
    } else {
      ...
      //add
    }

当前intset的编码为INTSET_ENC_INT16, 和新插入的encoding一致,满足条件,直接插入

3.3.3 判断新插入值是否已经存在,并获取插入位置

/* 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. */
if (intsetSearch(is,value,&pos)) {
    if (success) *success = 0;
    return is;
}
  • 已经存在则直接返回
  • 不存在,则返回应该插入的位置pos

这里pos=0,插入第一个位置

3.3.4 扩容intset

  is = intsetResize(is,intrev32ifbe(is->length)+1);
static intset *intsetResize(intset *is, uint32_t len) {
    uint32_t size = len*intrev32ifbe(is->encoding);
    is = zrealloc(is,sizeof(intset)+size);
    return is;
}

请添加图片描述

3.3.5 移动数据(插入位置不是结尾)

当插入位置不是结尾,则需要将插入位置后的数据依次往后移动。
本例中是尾部,所以不需要移动

3.3.6 插入数据

_intsetSet(is,pos,value);
/* Set the value at pos, using the configured encoding. */
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);
    }
}

请添加图片描述

3.3.7 更新intset集合长度

 is->length = intrev32ifbe(intrev32ifbe(is->length)+1);

请添加图片描述

3.4 例2.插入“1000”

3.4.1 计算插入值的使用数据类型

1000在int16_t的范围内,所以encoding=INTSET_ENC_INT16

3.4.2 判断intset当前编码是否满足新插入的值

当前intset的encoding为INTSET_ENC_INT16,满足新插入的INTSET_ENC_INT16,所以不需要升级,直接插入

3.4.3 判断新插入值是否已经存在,并获取插入位置

当前intset中只有10,所以新插入1000不存在,并且插入位置pos=1

3.4.4 扩容intset

请添加图片描述

3.4.5 移动数据(插入位置不是结尾)

新插入的位置为结尾,所以不需要移动

3.4.6 插入数据

请添加图片描述

3.4.7 更新intset集合长度

请添加图片描述

3.5 例3.插入“100”

3.5.1 计算插入值的使用数据类型

100在int16_t范围内,所以encoding=INTSET_ENC_INT16

3.5.2 判断intset当前编码是否满足新插入的值

当前intset的encoding为INTSET_ENC_INT16,满足intset的encoding>= 插入数据的encoding,所以直接插入

3.5.3 判断新插入值是否已经存在,并获取插入位置

当前intset中只有10,1000,所以100需要插入,并且插入在10和1000直接,pos=1

3.5.4 扩容intset

请添加图片描述

3.5.5 移动数据(插入位置不是结尾)

if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
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);
}

请添加图片描述

3.5.6 插入数据

请添加图片描述

3.5.7 更新intset集合长度

请添加图片描述

3.6 例4.插入“1048575”

3.6.1 计算插入值的使用数据类型

1048575(0xFFFFF)已经超过int16_t,最终编码int32_t, encoding=INTSET_ENC_INT32

3.6.2 判断intset当前编码是否满足新插入的值

当前intset的encoding为INTSET_ENC_INT16, 小于插入数据的编码INTSET_ENC_INT32, 所以需要升级

3.6.3 升级intset

  • 需要将intset所有的数据类型都从int16_t 转化为int32_t
  • 没有判断是否存在(肯定不存在,已经超过当前数据类型范围)
3.6.3.1重置intset的编码
 is->encoding = intrev32ifbe(newenc);

请添加图片描述

3.6.3.2扩容

请添加图片描述

3.6.3.3移动数据
int prepend = value < 0 ? 1 : 0;
...
while(length--)
  _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

prepend根据数据正负决定,如果数据为负数,则把第一个位置腾出来,用于存新数据,如果是整数,则将最后一个位置留下存新数据。
因为intset有序,并且插入的数据超过当前范围,所有负数是最小(插头部),整数是最大(插尾部)
请添加图片描述

3.6.3.4插入数据
if (prepend)
   _intsetSet(is,0,value);
else
   _intsetSet(is,intrev32ifbe(is->length),value);

请添加图片描述
负数插入头,整数插入尾

3.6.3.5更新intset集合长度

请添加图片描述

3.7 查询

  • 使用折半查询, 时间复杂度log(N)
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. */
        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;
        }
    }

    if (value == cur) {
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
}

3.8 获取intset集合个数

uint32_t intsetLen(const intset *is) {
    return intrev32ifbe(is->length);
}

3.9 获取整个intset的数据大小

size_t intsetBlobLen(intset *is) {
	return sizeof(intset)+intrev32ifbe(is->length)*intrev32ifbe(is->encoding);
}

3.10 获取某个位置的值

static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) {
    int64_t v64;
    int32_t v32;
    int16_t v16;

    if (enc == INTSET_ENC_INT64) {
        memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64));
        memrev64ifbe(&v64);
        return v64;
    } else if (enc == INTSET_ENC_INT32) {
        memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32));
        memrev32ifbe(&v32);
        return v32;
    } else {
        memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16));
        memrev16ifbe(&v16);
        return v16;
    }
}

3.11 在某个位置设置值

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);
    }
}

3.12 删除元素

intset *intsetRemove(intset *is, int64_t value, int *success) {
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    if (success) *success = 0;

    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);
        is->length = intrev32ifbe(len-1);
    }
    return is;
}
  • 首先判断数据类型是否在当前intset的范围内,如果不在,则数据不在,不用删除
  • 获取数据位置pos
  • 如果需要删除的数据不是结尾,则需要将删除位置后面的数据往前移动
  • 缩小存储空间
  • 更新集合长度

3.12.1 例1.删除10

3.12.1.1 获取删除数据的编码

10的范围在int16_t中,所以encoding=INTSET_ENC_INT16

3.12.1.2 判断删除数据编码是否在当前intset中

当前intset的encoding为INTSET_ENC_INT32, INTSET_ENC_INT32 >= INTSET_ENC_INT16, 所以满足需求

3.12.1.3 搜索数据,获取删除数据的位置

通过折半查找,10在集合中,并且pos=0

3.12.1.4 移动数据(删除的不是最后一个元素)

if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
这里pos=0 < 4-1, 所以需要移动
请添加图片描述

3.12.1.5 intset缩容

is = intsetResize(is,len-1);
请添加图片描述

3.12.1.6 更新长度

is->length = intrev32ifbe(len-1);
请添加图片描述

3.12.2 例2.删除1048575

3.12.2.1 获取删除数据的编码

1048575在int32_t的范围内,所以encoding=INTSET_ENC_INT32

3.12.2.2 判断删除数据编码是否在当前intset中

当前intset的encoding为INTSET_ENC_INT32, 满足条件

3.12.2.3 搜索数据,获取删除数据的位置

查询后,1048575为最后一个元素,pos=2

3.12.2.4 移动数据(删除的不是最后一个元素)

删除的数据是最后一个,不需要移动数据

3.12.2.5 intset缩容

请添加图片描述

3.12.2.6 更新长度

请添加图片描述

3.12.3 例3.删除68719476735

3.12.3.1 获取删除数据的编码

68719476735已经超过int32_t范围,所以encoding=INTSET_ENC_INT64

3.12.3.2 判断删除数据编码是否在当前intset中

当前intset的encoding为INTSET_ENC_INT32,小于INTSET_ENC_INT64,所以数据不存在,不需要删除

3.12.4 例4.删除120

3.12.4.1 获取删除数据的编码

120在int16_t的范围内,所以encoding=INTSET_ENC_INT16

3.12.4.2 判断删除数据编码是否在当前intset中

当前intset的encoding为INTSET_ENC_INT32, 满足条件,数据可以能在集合中

3.12.4.3 搜索数据,获取删除数据的位置

通过查询,120没有在整个集合中,所以不用删除

随机获取一个元素

/* Return random member */
int64_t intsetRandom(intset *is) {
    return _intsetGet(is,rand()%intrev32ifbe(is->length));
}
  • 因为intset是连续的,可以随机访问

实践

  • 有序集合都是整数时,使用intset
  • 当增加字符串时,转换成了hashtable
$ ./redis-cli
127.0.0.1:6379> sadd set1 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> object encoding set1
"intset"
127.0.0.1:6379> sadd set1 apple
(integer) 1
127.0.0.1:6379> object encoding set1
"hashtable"
127.0.0.1:6379>
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值