redis中集合类型set底层实现(一)intset的底层实现逻辑

一.intset特性

1.仅储存整型(根本要求)

2.内部升序排序

3.三种编码节省空间(16/ 32 /64bits)

4.自动升级降级

5.集合最基本的特性:唯一性

二.intset定义

#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
class intset {
public:
    uint32_t encoding;

    uint32_t length;

    int8_t contents[];
}

        为了最大化的利用有限的空间,intset具有三种整型编码方式,分别使用16bits(-32768~32767), 32bits(-2147483648~2147483647),64bits(-2^{63}~2^{63}-1)。可见,每种编码方式都是8bits的整数倍, 所以在intset类中,我们使用int8_t类型的柔性数组content[]作为我们的实际存储空间,容易知道,接下来我们对intset的所有操作其实是用过编码位数和对内存的直接操作。其中length指的是当前intset包含的数据个数。

三.intset的基本操作

        任何数据结构几乎都无法避免增删改查,一个良好的存储数据结构必须能够承受海量的数据,以及拥有更快的操作。而我们所讲的intset也无非是增删改查。只不过是对内存和操作速度优化后的“数组”罢了。

intset* intsetNew(void);//创建一个空集合
uint8_t intsetGetAndSetV(intset* is, uint32_t pos, int64_t* value);//获取并传值
intset* intsetAdd(intset* is, int64_t value, uint8_t* success);//添加元素
intset* intsetRemove(intset* is, int64_t value, int* success);//删元素
uint8_t intsetFind(intset* is, int64_t value);//看某个元素是否在集合中
int64_t intsetRandom(intset* is);//随机取数
int64_t intsetGet(intset* is, int pos);//取索引为pos的值
uint32_t intsetLen(intset* is);//集合元素个数
size_t intsetBlobLen(intset* is);//返回内存字节数

1.查

        首先是最基本的“查”操作,涉及到一下几个函数,是后面很多操作的基石

int64_t intset::intsetGet(intset* is, int pos);//外部api
int64_t intset::intsetGetEncoded(intset* is, int pos, uint8_t enc);//仅内部
uint8_t intset::intsetFind(intset* is, int64_t value);//外部api
uint8_t intset::intsetSearch(intset* is, int64_t value, uint32_t* pos);//仅内部

         还记得我们前面所说吗,intset的绝大多数操作实际上是对于内存空间的直接操作,其中intsetGetEncoded函数就是通过巧妙的类型转换内存拷贝而获得,代码如下:

int64_t intset::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;
    }
}
注:memrev32ifbe等类似函数负责端序转换,视操作系统而定

        其中最为重要的是intsetSearch函数,找到返回1,没找到返回0,同时会更新pos的值,找到时是pos是目标值的索引,没找到时是目标值应该插入的位置。已知内部数据按升序排序,所以只需要通过二分查找即可。

//查找,搜索
//成功查找,则设置pos,返回1.
//找不到,pos为插入位置
uint8_t intset::intsetSearch(intset* is, int64_t value, uint32_t* pos)
{
    int min = 0, max = intrev32ifbe(is->length) - 1, mid = -1;
    int64_t cur = -1;

    // 处理 is 为空时的情况
    if (intrev32ifbe(is->length) == 0) {
        if (pos) *pos = 0;
        return 0;
    }
    else {
        //数组有序,
        if (value > intsetGet(is, intrev32ifbe(is->length) - 1)) {
            //大于最大值
            if (pos) *pos = intrev32ifbe(is->length);
            return 0;
        }
        else if (value < intsetGet(is, 0)) {
            //小于最小值
            if (pos) *pos = 0;
            return 0;
        }
    }

    // 在有序数组中进行二分查找
    while (max >= min) {
        mid = (min + max) / 2;
        cur = intsetGet(is, mid);
        if (value > cur) {
            min = mid + 1;
        }
        else if (value < cur) {
            max = mid - 1;
        }
        else {
            break;
        }
    }

    // 检查是否已经找到了 value
    if (value == cur) {
        if (pos) *pos = mid;
        return 1;
    }
    else {
        if (pos) *pos = min;
        return 0;
    }
}

注: intrev32ifbe是端序转换函数,视操作系统而定

2.改

        集合中的改其实并不真正存在,实际是search and delete, 然后add,虽然我们支持对于指定位置pos的获取,但并不直接支持改操作。读者有兴趣的话可以自行尝试实现。

3.删

        删除其实就是利用前面我们所介绍的intsetSearch函数,获取到目标位置后,通过内存移动,对要进行删除操作的数据进行覆盖,然后重新分配空间。内存移动我们会放在最后一部分来讲。

intset* intset::intsetRemove(intset* is, int64_t value, int* success = NULL)
{
    // 计算 value 的编码方式
    uint8_t valenc = intsetValueEncoding(value);
    uint32_t pos;

    // 默认设置标识值为删除失败
    if (success) *success = 0;

    // 当 value 的编码大小小于或等于集合的当前编码方式(说明 value 有可能存在于集合)
    // 并且 intsetSearch 的结果为真,那么执行删除
    if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is, value, &pos)) {

        // 取出集合当前的元素数量
        uint32_t len = intrev32ifbe(is->length);

        // 设置标识值为删除成功
        if (success) *success = 1;

        if (pos < (len - 1)) intsetPartialMoveTail(is, pos + 1, pos);
        // 缩小数组的大小,移除被删除元素占用的空间
        is = intsetResize(is, len - 1);
        // 更新集合的元素数量
        is->length = intrev32ifbe(len - 1);
    }

    return is;
}

4.增

intset* intset::intsetAdd(intset* is, int64_t value, uint8_t* success = NULL)

intset添加数据需要经历一下几步操作:

升级并添加 

 第一步判断value的编码是关键的,因为当value的编码大于当前intset的编码时,意味着所需要添加的新值要么是最大值,要么是最小值,换句话说不是头插就是尾插。所以我们需要做的事也很明确,首先判断是头插还是尾插,然后升级编码(重新分配内存,并将原有的数据按照顺序重新设置),新值。代码如下:

升级并添加情况
intset* intset::intsetUpgradeAndAdd(intset* is, int64_t value)
{
    // 当前的编码方式
    uint8_t curenc = intrev32ifbe(is->encoding);

    // 新值所需的编码方式
    uint8_t newenc = intsetValueEncoding(value);

    // 当前集合的元素数量
    int length = intrev32ifbe(is->length);

    //升级要么最大,要么最小
    //确保新值的位置,要么头插,要么尾插
    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));

    // 设置新值,根据 prepend 的值来决定是添加到数组头还是数组尾
    if (prepend)
        intsetSet(is, 0, value);
    else
        intsetSet(is, intrev32ifbe(is->length), value);

    // 更新整数集合的元素数量
    is->length = intrev32ifbe(intrev32ifbe(is->length) + 1);

    return is;
}

 完整的添加函数

//添加,success参数默认为NULL(测试使用)
intset* intset::intsetAdd(intset* is, int64_t value, uint8_t* success = NULL)
{
    // 计算编码 value 所需的长度
    uint8_t valenc = intsetValueEncoding(value);
    uint32_t pos;//插入位置

    // 默认设置插入为成功
    if (success) *success = 1;

     // 如果 value 的编码比整数集合现在的编码要大
     // 那么表示 value 必然可以添加到整数集合中,需要升级
    if (valenc > intrev32ifbe(is->encoding)) 
        return intsetUpgradeAndAdd(is, value);
    else {
        if (intsetSearch(is, value, &pos)) {
            //已经存在于集合中
            if (success) *success = 0;
            return is;
        }
        //重新分配空间(扩容)
        is = intsetResize(is, intrev32ifbe(is->length) + 1);
        //如不是尾插,移动后缀字节
        if (pos < intrev32ifbe(is->length)) 
            intsetPartialMoveTail(is, pos, pos + 1);
    }

    // 将新值设置到底层数组的指定位置中
    intsetSet(is, pos, value);

    // 更新集合长度
    is->length = intrev32ifbe(intrev32ifbe(is->length) + 1);

    // 返回添加新元素后的整数集合
    return is;

}

 5.尾部空间移动(非升级增和删时使用)

 向前或先后移动指定索引范围内的数组元素,函数名中的 MoveTail 其实是一个有误导性的名字,

 这个函数可以向前或向后移动元素,而不仅仅是向后在添加新元素到数组时,就需要进行向后移动,

 * 如果数组表示如下(?表示一个未设置新值的空间):

 * | x | y | z | ? |

 *     |<----->|

 * 而新元素 n 的 pos 为 1 ,那么数组将移动 y 和 z 两个元素

 * | x | y | y | z |

 *         |<----->|

 * 接着就可以将新元素 n 设置到 pos 上了:

 * | x | n | y | z |

 * 当从数组中删除元素时,就需要进行向前移动,

 * 如果数组表示如下,并且 b 为要删除的目标:

 * | a | b | c | d |

 *         |<----->|

 * 那么程序就会移动 b 后的所有元素向前一个元素的位置,

 * 从而覆盖 b 的数据:

 * | a | c | d | d |

 *     |<----->|

 * 最后,程序再从数组末尾删除一个元素的空间:

 * | a | c | d |

 * 这样就完成了删除操作。代码如下:

//移动后缀子字节,用于编码为改变时删除或添加
void intset::intsetPartialMoveTail(intset* is, uint32_t from, uint32_t to)
{
    void* src, * dst;

    // 要移动的元素个数
    uint32_t bytes = intrev32ifbe(is->length) - from;

    // 集合的编码方式
    uint32_t encoding = intrev32ifbe(is->encoding);

    // 根据不同的编码
    // src = (Enc_t*)is->contents+from 记录移动开始的位置
    // dst = (Enc_t*)is_.contents+to 记录移动结束的位置
    // bytes *= sizeof(Enc_t) 计算一共要移动多少字节
    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);
    }

    // 进行移动
    // T = O(N)
    memmove(dst, src, bytes);
}

四.结语

        通过对内存空间和c/c++特性的极致应用,体会到intset设计的精妙了吗。关注我,持续更新redis底层原理。

Redis的有序集合(zset)和集合(set底层数据结构实现是不同的。 对于有序集合(zset),它使用了两种数据结构来实现:字典(dict)和跳跃表(skiplist)。字典用来保存元素到分数的映射关系,而跳跃表则用来进行范围操作和根据分数查询数据。由于字典是无序的,每次进行范围操作时都需要进行排序,而跳跃表虽然能执行范围操作,但查找操作的时间复杂度是O(logN)。因此,Redis使用了字典和跳跃表的组合来共同实现有序集合。 而对于集合(set),Redis使用的是字典(dict)来实现。字典是一种无序的数据结构,它可以快速地进行成员的添加、删除和查找操作,时间复杂度均为O(1)。 综上所述,Redis的zset底层数据结构实现使用了字典和跳跃表的组合,而set底层数据结构实现则只使用了字典。这样设计的目的是为了在有序集合能够同时拥有快速的成员查找和范围操作的能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Redis第十二讲 Redis之zset底层数据结构实现](https://blog.csdn.net/huanglu0314/article/details/129967415)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [Redis的zset底层是通过什么数据结构来实现的?](https://blog.csdn.net/qq_27198345/article/details/108674817)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值