Redis数据结构之——intset

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Redis数据结构之——intset

写在前面

以下内容是基于Redis 6.2.6 版本整理总结

一、整数集合(intset)

当一个集合只包含整数值元素,并且元素的个数不多时,Redis会使用整数集合作为集合键的底层实现。

1.1 整数集合的实现

整数集合可用保存的数据类型有:int16_t int32_t 和 int64_t 的整数值,并且保证集合中不会出现重复元素。

整数集合定义如下:

// src/intset.h
typedef struct intset {
    uint32_t encoding; // 编码方式,后面会详细解释
    uint32_t length;   // 集合中元素的个数,也就是contents数组的长度
    int8_t contents[]; // 保存元素的数组
} intset;
/* Note that these encodings are ordered, so:
 * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

分析:

contents数组是整数集合的底层实现:整数集合中的每一个元素就是contents数组中的一个元素,每个元素在数组中按照从小到大的顺序排列,并且没有重复元素。

虽然,contents数组被声明为 int8_t 类型的数组,但实际上contents数组并不保存任何int8_t 类型的值,contents数组实际存储的类型取决于encoding的值。encoding的取值可以是:INTSET_ENC_INT16、INTSET_ENC_INT32 或 INTSET_ENC_INT64,每种编码的取值范围如下:

  1. INTSET_ENC_INT16 取值范围:[−2^15 ,2^15−1] 即:[-32768, 32767 ]
  2. INTSET_ENC_INT32 取值范围:[−2^31 ,2^31−1] 即:[-2147483648, 2147483647]
  3. INTSET_ENC_INT64 取值范围:[−2^63 ,2^63−1] 即:[-9223372036854775808, 9223372036854775807]

说明:n比特有符号整数的表示范围为:[−2^(n−1) ,2^(n−1)−1]

整数集合图示

1.2 整数集合的操作

每次往整数集合中插入新元素时,会检查新元素的类型,是不是比当前contents数组的类型长,如果是,整数集合需要先进行升级操作。比如:现在我们要把编码为int32_t的元素插入到整数集合里,因为65535的类型比集合中当前元素类型要长,所以要先对整数集合升级。

升级的好处

  1. 灵活:C语言是静态类型的语言,为了避免类型错误,我们通常是一种类型就放到一种类型的数据结构里,如:我们一般会使用int16_t类型的数组保存int16_t类型的值,用int32_t类型数组保存int32_t类型的值。而整数集合允许随意的将不同类型的整型值添加到集合中,而不用担心类型错误。
  2. 节约内存:如果想让一个数组同时保存,int16_t、int32_t和int64_t的值,最简单的方式就是使用int64_t类型的数组,但是这会造成空间的浪费。inset只会在适当的时候才会触发升级操作,其他时间都是尽可能节约内存。
1.2.1 intset添加元素

inset插入元素

/* Insert an integer in the intset */
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. */
    // 待插入整数的编码类型大于当前contents数组的编码类型,需要先进行升级操作
    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. */
        // 插入之前先在集合里查找是否已经存在该元素,(二分查找)
        // 如果该元素已经存在,则直接返回;如果没有,则返回该元素待插入的数组下标,保存在pos中
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }
        // 1 3 5   insert 2  pos: 1
        // 扩展空间
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        // 如果pos 小于当前数组长度,将从pos位置开始的元素,同一往后挪一格,腾出pos的位置
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    // 将待插入的元素,放在pos的位置
    _intsetSet(is,pos,value);
    // 更新数组的长度
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}
// 整数集合升级
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    // 获取当前整数集合的编码
    uint8_t curenc = intrev32ifbe(is->encoding);
    // 计算待插入元素value的编码
    uint8_t newenc = _intsetValueEncoding(value);
    // 获取元素个数
    int length = intrev32ifbe(is->length);
    // 待插入的元素是否为负数
    int prepend = value < 0 ? 1 : 0;
    /* First set new encoding and resize */
    // 重新设置整数集合的编码及扩展空间
    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. */
    // 从后往前更新,不会出现覆盖某个元素的情况
    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;
}
// 二分查找插入位置
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;
    }
}
// 1 3 5  length:3    insert: 2    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);
}

插入元素步骤图解

1.2.2 inset 删除元素:
/* Delete integer from intset */
intset *intsetRemove(intset *is, int64_t value, int *success) {
  // 获取待删除元素的编码
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    if (success) *success = 0;
  // 如果找到待删除元素及其pos
    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;
}

二、总结

  1. 整数集合的底层实现是数组,这个数组以有序、无重复的方式存储元素,在需要时会根据新添加元素的类型升级数组的类型。
  2. 只支持升级操作,不支持降级操作
  3. 整数集合是有序集合的底层实现之一

文章参考与<零声教育>的C/C++linux服务期高级架构系统教程学习:

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
相关文章
|
2月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
4月前
|
存储 NoSQL 算法
Redis设计与实现——数据结构与对象
Redis 是一个高性能的键值存储系统,其数据结构设计精妙且高效。主要包括以下几种核心数据结构:SDS、链表、字典、跳跃表、整数集合、压缩列表。此外,Redis 对象通过类型和编码方式动态转换,优化内存使用,并支持引用计数、共享对象和淘汰策略(如 LRU/LFU)。这些特性共同确保 Redis 在性能与灵活性之间的平衡。
|
7月前
|
NoSQL 算法 安全
Redis原理—1.Redis数据结构
本文介绍了Redis 的主要数据结构及应用。
Redis原理—1.Redis数据结构
|
9月前
|
存储 消息中间件 缓存
Redis 5 种基础数据结构?
Redis的五种基础数据结构——字符串、哈希、列表、集合和有序集合——提供了丰富的功能来满足各种应用需求。理解并灵活运用这些数据结构,可以极大地提高应用程序的性能和可扩展性。
152 2
|
4月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
4月前
|
缓存 NoSQL Java
Redis+Caffeine构建高性能二级缓存
大家好,我是摘星。今天为大家带来的是Redis+Caffeine构建高性能二级缓存,废话不多说直接开始~
558 0
|
4月前
|
消息中间件 缓存 NoSQL
基于Spring Data Redis与RabbitMQ实现字符串缓存和计数功能(数据同步)
总的来说,借助Spring Data Redis和RabbitMQ,我们可以轻松实现字符串缓存和计数的功能。而关键的部分不过是一些"厨房的套路",一旦你掌握了这些套路,那么你就像厨师一样可以准备出一道道饕餮美食了。通过这种方式促进数据处理效率无疑将大大提高我们的生产力。
159 32
|
4月前
|
缓存 NoSQL Java
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
92 5
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
|
6月前
|
缓存 监控 NoSQL
Redis--缓存击穿、缓存穿透、缓存雪崩
缓存击穿、缓存穿透和缓存雪崩是Redis使用过程中可能遇到的常见问题。理解这些问题的成因并采取相应的解决措施,可以有效提升系统的稳定性和性能。在实际应用中,应根据具体场景,选择合适的解决方案,并持续监控和优化缓存策略,以应对不断变化的业务需求。
871 29