源码版本:4.0.1
源码位置:

1. 整数集合简介

intset是Redis内存数据结构之一,和之前的 sdsskiplistdictadlist 等通用数据相比,它是Redis特有的,用来实现Redis的Set结构(当元素较小且为数字类型时),它的特点有:

  • 元素类型只能为数字。
  • 元素有三种类型:int16_t、int32_t、int64_t。
  • 元素有序,不可重复。
  • intset和sds一样,内存连续,就像数组一样。

2. 数据结构定义

typedef struct intset {
uint32_t encoding; // 编码类型 int16_t、int32_t、int64_t
uint32_t length; // 长度 最大长度:2^32
int8_t contents[]; // 柔性数组
} intset;

3. 创建、插入(扩缩容)、查找(二分查找)、删除

以下面这个例子来看下intset的各种操作:

(需要自己在server.c中添加intset.h头文件,然后将main函数修改成下面代码)

int main(int argc, char **argv)  {

    uint8_t ret;
uint8_t success;
int64_t value;
int16_t int16_a = 2 * 128;
int16_t int16_b = 2 * 256;
int32_t int32_c = 2 * 65536; printf("----------intset insert----------\n");
intset *is = intsetNew(); is = intsetAdd(is, int16_a, &success);
if (success == 0) {
printf("add int16_a fail\n");
} else {
printf("add int16_a success, ");
} printf("is encoding:%d, length:%d, bloblen:%zu\n", is->encoding, intsetLen(is), intsetBlobLen(is)); is = intsetAdd(is, int32_c, &success);
if (success == 0) {
printf("add int32_c fail\n");
} else {
printf("add int32_c success, ");
} printf("is encoding:%d, length:%d, bloblen:%zu\n", is->encoding, intsetLen(is), intsetBlobLen(is)); is = intsetAdd(is, int16_b, &success);
if (success == 0) {
printf("add int16_b fail\n");
} else {
printf("add int16_b success, ");
} printf("is encoding:%d, length:%d, bloblen:%zu\n", is->encoding, intsetLen(is), intsetBlobLen(is)); printf("----------intset found----------\n");
ret = intsetFind(is, int16_b);
if (ret == 1) {
printf("int16_b is found\n");
} printf("----------intset get----------\n"); ret = intsetGet(is, 0, &value);
if (ret != 0) {
printf("int16_a get value is %lld\n", value);
} printf("----------intset remove----------\n"); is = intsetRemove(is, int16_b, &success);
if (success == 1) {
printf("int16_b is success remove\n");
} printf("is encoding:%d, length:%d, bloblen:%zu\n", is->encoding, intsetLen(is), intsetBlobLen(is)); zfree(is); return 0;
} Out >
----------intset insert----------
add int16_a success, is encoding:2, length:1, bloblen:10
add int32_c success, is encoding:4, length:2, bloblen:16
add int16_b success, is encoding:4, length:3, bloblen:20
----------intset found----------
int16_b is found
----------intset get----------
int16_a get value is 256
----------intset remove----------
int16_b is success remove
is encoding:4, length:2, bloblen:16

3.1 创建

  • intset *is = intsetNew(),创建了一个空的名为is的intset,代码如下:
/* Create an empty intset. */
intset *intsetNew(void) {
intset *is = zmalloc(sizeof(intset)); // 分配空间
is->encoding = intrev32ifbe(INTSET_ENC_INT16); // 初试创建默认元素大小为 2 字节
is->length = 0;
return is;
}

3.2 插入

  • 接下来我们调用intsetAdd()连续插入了三次数据,它的代码如下:
/* 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. */
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. */
if (intsetSearch(is,value,&pos)) {
if (success) *success = 0;
return is;
} is = intsetResize(is,intrev32ifbe(is->length)+1);
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
} _intsetSet(is,pos,value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}

整个函数的流程如下:

  • uint8_t valenc = _intsetValueEncoding(value),根据value的长度获取其对应的编码,保存至valenc。
  • if (valenc > intrev32ifbe(is->encoding)),如果valenc > is->encoding,表明目前的encoding太小,需要整体提高encoding的大小。
    • 执行intsetUpgradeAndAdd()完成扩大操作。
  • 如果valenc <= is->encoding。
    • 执行查找intsetSearch(is,value,&pos),如果查找到元素,将success置为0,表示插入失败,即此元素已经存在。
    • 如果没有查找到,pos表示元素应该插入的位置,则给is扩容一个元素的大小intsetResize(is,intrev32ifbe(is->length)+1),如果需要则使用intsetMoveTail(is,pos,pos+1)将元素挪移。
  • _intsetSet(is,pos,value),将元素插入intset。
  • is->length = intrev32ifbe(intrev32ifbe(is->length)+1),更新length的值。

插入了第一个元素int16_ais如下图所示:

与输出结果相对应:

add int16_a success, is encoding:2, length:1, bloblen:10
  • 接下来我们的代码添加了第二个元素,由于它的大小超过了INTSET_ENC_INT16,所以添加操作会执行intsetUpgradeAndAdd()函数扩大encoding:
/* Upgrades the intset to a larger encoding and inserts the given integer. */
static 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; /* 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;
}
  • 将is目前的encoding保存至curenc,将value的encoding保存至newenc。
  • int prepend = value < 0 ? 1 : 0,prepend用来确定新value的插入位置:第一个还是最后一个,因为它的encoding比is->encoding要大,所以它要么比目前所有元素都大,要么比所有元素都小,即插入位置要么第一个,要么最后一个。
  • 然后更新encoding的值,重新分配空间。
  • 挪动所有的元素到新位置。
  • 根据prepend的值判断将value插入第一个位置还是最后一个位置。
  • 更新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. */
// 根据集合原来的编码方式,从底层数组中取出集合元素
// 然后再将元素以新编码的方式添加到集合中
// 当完成了这个步骤之后,集合中所有原有的元素就完成了从旧编码到新编码的转换
// 因为新分配的空间都放在数组的后端,所以程序先从后端向前端移动元素
// 举个例子,假设原来有 curenc 编码的三个元素,它们在数组中排列如下:
// | x | y | z |
// 当程序对数组进行重分配之后,数组就被扩容了(符号 ? 表示未使用的内存):
// | x | y | z | ? | ? | ? |
// 这时程序从数组后端开始,重新插入元素:
// | x | y | z | ? | z | ? |
// | x | y | y | z | ? |
// | x | y | z | ? |
// 最后,程序可以将新元素添加到最后 ? 号标示的位置中:
// | x | y | z | new |
// 上面演示的是新元素比原来的所有元素都大的情况,也即是 prepend == 0 // 当新元素比原来的所有元素都小时(prepend == 1),调整的过程如下:
// | x | y | z | ? | ? | ? |
// | x | y | z | ? | ? | z |
// | x | y | z | ? | y | z |
// | x | y | x | y | z |
// 当添加新值时,原本的 | x | y | 的数据将被新值代替
// | new | x | y | z |

插入了第二个元素之后is如下图所示:

输出如下所示:

add int32_c success, is encoding:4, length:2, bloblen:16
  • 接下来我们插入第三个元素,此时的encoding满足int16_b的大小,所以代码分支去执行查找操作intsetSearch()函数:
/* Search for the position of "value". Return 1 when the value was found and
* sets "pos" to the position of the value within the intset. Return 0 when
* the value is not present in the intset and sets "pos" to the position
* where "value" can be inserted. */
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,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 = ((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;
}
}
  • 如果目前is->length为0,则标记pos为0,并且返回查找失败。
  • 如果value比最大值还大,或者比最小值还小,标记pos为length或者0,返回查找失败。
  • 否则使用二分法查找到元素,将pos指向应当插入的位置。

等到intsetSearch()返回之后,pos表示value应当插入的位置,此时需要挪动pos之后的元素向后一个位置,挪动函数是intsetMoveTail()

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

实际上是把内存整体向后移动了一个元素的位置,需要注意的是 memmove 函数允许src和dst之间的内存有重叠部分。

再来一段生动的图解,同样出自参考[1]:

/*
* 向前或先后移动指定索引范围内的数组元素
*
* 函数名中的 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 |
* 这样就完成了删除操作。
*
* T = O(N)
*/

此时的is如下图所示:

3.3 查找

查找的逻辑在上面插入操作时候已经说到了,实际上是二分查找

3.4 删除

/* 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; 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;
}
  • 首先获取元素的encoding,如果不符合条件,success为0表示删除失败。
  • 否则调用intsetSearch()查找到相应的位置
  • 然后将pos+1的元素移动到pos位置上,相当于向前覆盖一个元素。
  • 将元素个数减一,重新分配内存。

4. 总结

本篇博客分析了intset的数据结构以及基本操作,整个数据结构还是比较简单的。
个人觉得intset实现按照元素不断增大可以扩大encoding对内存非常友好,但是它没有提供对应的减小encoding操作,即可以一直扩大encoding编码类型,但是不能缩小,这一点不太好。

参考资料:
[1] Redis源码注释3.0-黄健宏

[完]

Redis源码分析(intset)的更多相关文章

  1. Redis源码分析:serverCron - redis源码笔记

    [redis源码分析]http://blog.csdn.net/column/details/redis-source.html   Redis源代码重要目录 dict.c:也是很重要的两个文件,主要 ...

  2. redis源码分析之事务Transaction(下)

    接着上一篇,这篇文章分析一下redis事务操作中multi,exec,discard三个核心命令. 原文地址:http://www.jianshu.com/p/e22615586595 看本篇文章前需 ...

  3. Redis源码分析系列

    0.前言 Redis目前热门NoSQL内存数据库,代码量不是很大,本系列是本人阅读Redis源码时记录的笔记,由于时间仓促和水平有限,文中难免会有错误之处,欢迎读者指出,共同学习进步,本文使用的Red ...

  4. redis源码分析之发布订阅(pub/sub)

    redis算是缓存界的老大哥了,最近做的事情对redis依赖较多,使用了里面的发布订阅功能,事务功能以及SortedSet等数据结构,后面准备好好学习总结一下redis的一些知识点. 原文地址:htt ...

  5. redis源码分析之事务Transaction(上)

    这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易 ...

  6. redis源码分析之有序集SortedSet

    有序集SortedSet算是redis中一个很有特色的数据结构,通过这篇文章来总结一下这块知识点. 原文地址:http://www.jianshu.com/p/75ca5a359f9f 一.有序集So ...

  7. Redis源码分析(dict)

    源码版本:redis-4.0.1 源码位置: dict.h:dictEntry.dictht.dict等数据结构定义. dict.c:创建.插入.查找等功能实现. 一.dict 简介 dict (di ...

  8. redis源码分析

    我阅读的源码版本是redis-2.8.19 src目录下总共96个.h,.c文件 1. 数据结构相关源码(15个左右)字符串代码: sds.h, sds.c字典:dict.h, dict.c链表: a ...

  9. redis源码分析(一)-sds实现

    redis支持多种数据类型,sds(simple dynamic string)是最基本的一种,redis中的字符串类型大多使用sds保存,它支持动态的扩展与压缩,并提供许多工具函数.这篇文章将分析s ...

随机推荐

  1. Nginx系列(2)- 正向代理和反向代理

    Nginx作用 Http代理,反向代理:作为web服务器最常用的功能之一,尤其是反向代理 正向代理是代理客户端,反向代理是代理服务端 正向代理要知道访问服务器的地址,反向代理不需要知道访问服务器的真实 ...

  2. 进入vim /etc/profile如何退出

    按o或i输入 按Esc,输入:wq,退出

  3. 虚拟机启动jenkins,访问提示:该Jenkins实例似乎已离线

    ubuntu虚拟机使用java命令启动jenkins,本地访问提示:该Jenkins实例似乎已离线. https://www.cnblogs.com/du-hong/p/10655635.html 没 ...

  4. re.findall用法

    其中,re.findall() 函数可以遍历匹配,可以获取字符串中所有匹配的字符串,返回一个列表. 在python源代码中,展示如下: 搜索string,返回一个顺序访问每一个匹配结果(Match对象 ...

  5. 我在学习Blazor当中踩的巨坑!Blazor WebAssembly调试

    最近嘛,看看Blazor已经蛮成熟的.顺便想在自家的框架里使用这个东西,毕竟我还是很念旧的,而且Blazor的技术栈也不麻烦.然后呢,在调试这一关我可是踩了大坑. 我的VS是2019,很早以前装的.然 ...

  6. CF643F-Bears and Juice【组合数学】

    正题 题目链接:https://www.luogu.com.cn/problem/CF643F 题目大意 题目有点奇怪就直接放翻译了 有 \(n\) 只熊和若干桶果汁和恰好一桶酒,每一天每只熊会选择一 ...

  7. android-- 按需打包的框架搭建--新手教程

    1, 新建项目VariantTest 2, 生成keystore 可以看到, 默认的build variant只有debug一种 当我试图选release的时候,发现报错了 什么错呢 大致意思是说我们 ...

  8. iframe、SameSite与CEF

    iframe.SameSite与CEF 背景 本人使用CEF(或是Chrome)来加载开发的前端页面,其中使用iframe嵌入了第三方页面,在第三方页面中需要发送cookie到后端,然而加载会报错,第 ...

  9. Apache ShardingSphere 邀您相约 Open Source Day

    Apache ShardingSphere 很荣幸接受 AnitaB.org 的邀请参加 OpenSourceDay Summer 2021 活动. Open Source Day (OSD) 是一个 ...

  10. Python 实现断网自动重连

    为了实现 断网了,自动连接网络原理:每隔一段时间ping一下百度,判断网络状态,没有联网的话,就模仿浏览器发一条Post给服务器import urllibimport hashlibimport su ...