当一个集合中只包含整数,并且元素的个数不是很多的话,redis 会用整数集合作为底层存储,它的一个优点就是可以节省很多内存,虽然字典结构的效率很高,但是它的实现结构相对复杂并且会分配较多的内存空间。

而我们的整数集合(intset)可以做到使用较少的内存空间却达到和字典一样效率的实现,但也是前提的,集合中只能包含整型数据并且数量不能太多。整数集合最多能存多少个元素在 redis 中也是有体现的。

OBJ_SET_MAX_INTSET_ENTRIES 512

也就是超过 512 个元素,或者向集合中添加了字符串或其他数据结构,redis 会将整数集合向字典结构进行转换。

一、基本的数据结构

intset 的结构定义很简单,有以下成员构成:

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

encoding 记录当前 intset 使用编码,有三个取值:

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

length 记录整数集合中目前存储了多少个元素,contents 记录我们实际的数据集合,虽然我们看到结构体中给数组元素的类型定死成 int8_t,但实际上这个 int8_t 定义的毫无意义,因为这里的处理方式非常规的数组操作,content 字段虽然被定义成指向一个 int8_t 类型数据的指针,但实际上 redis 无论是读取数组元素还是新增元素进去都依赖 encoding 和 length 两个字段直接操作的内存。

基本数据结构还是非常的简单的,下面我们来看看它的一些核心方法。

二、核心 API 实现

1、初始化一个 intset

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

可见,默认的 inset 配置是使用 INTSET_ENC_INT16 作为数据存储大小,并且不会为 content 数组初始化。常规的数组需要先预先确定数组长度,然后分配内存,继而通过 contents[x] 可以访问数组中任一元素。

但是,inset 这里是非常规式操作数组,encoding 字段定义了数组中每个元素实际类型,lenth 字段定义了数组中实际的元素个数,那么 contents[x] 是失效的,这种方式只会按照 int8_t 进行内存偏移,这种方式是拿不到正确的数据的,所以 redis 中通过 memcpy 按照 encoding 字段的值暴力直接偏移地址操作内存读取数据。

所以,这也是为什么 intset 初始化时不初始化 content 数组的原因所在,因为没有必要。而每当新增一个元素的时候都会去动态扩容原数组的长度以盛放下新插入进来的元素,扩容不会扩容很多,刚好一个新元素所占用的内存即可。具体的细节,我们接着看。

2、添加新元素

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
//计算得到新插入的元素的编码
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
if (success) *success = 1;
//如果大于 intset 目前存储元素的编码大小
if (valenc > intrev32ifbe(is->encoding)) {
//触发 intset 升级
return intsetUpgradeAndAdd(is,value);
} else {
//二分搜索当前元素,如果元素已经存在会直接返回
//如果没找到元素,pos 的值就是该元素的位置索引
if (intsetSearch(is,value,&pos)) {
if (success) *success = 0;
return is;
}
//resize 集合,扩容一个元素的内存空间
is = intsetResize(is,intrev32ifbe(is->length)+1);
//移动 pos 后面的元素,以插入我们的新元素
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
}
//赋值
_intsetSet(is,pos,value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}

由此,我们应该知道为什么 intset 内的数据是有序且无重复的了,二分查找 O(logN),但是 intset 插入一个元素却不是 O(logN),因为有些情况会触发升级操作,或者极端情况下,会移动所有元素,时间复杂度达到 O(N)。

3、升级

我们先看示意图的变化,然后再分析源码,假设原 intset 使用 16 位的编码存储数据,先来了一个 32 位的数据,触发了我们的编码升级。

原 intset 结构如下:

新 intset 结构会扩容成这样:

虽然数据占用的内存已经分配好了,但是还需要做的是迁移每个元素占用的比特位。

做法是这样的,假设我们的新元素是 int_32 类型的数值 65536,那么首先我们会将这个 65536 放到[128-159]比特位区间,然后将 78 放到[96-127]比特位区间,并向前以此类推,最后我们会得到升级完成之后 intset。

下面我们看 redis 中代码的实现:

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
//intset目前的编码
uint8_t curenc = intrev32ifbe(is->encoding);
//intset即将扩展到的编码
uint8_t newenc = _intsetValueEncoding(value);
int length = intrev32ifbe(is->length);
int prepend = value < 0 ? 1 : 0; //根据新的元素内存大小重新分配 intset 内存大小
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1);
//这个地方我先标记一下 @1,下面详细分析
//总体上你可以理解,就是我们上图画的那样,从原集合的最后一个元素
//开始扩大它占用的比特位
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc)); //将新元素放进 intset 中
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}

别的不再解释,我重点解释一下我做标记的 @1,这个循环其实是这个方法的核心点,它完成了将旧元素扩充比特位这么一个操作。

首先明确的一点是,升级操作只有两种情况会触发,一种是新插入一个较大的数值,另一种是新插入一个负很大的值,这两种情况都会导致类型不够存储,需要扩大数据位。

_intsetGetEncoded 这个方法可以根据给定了 length,也就是元素在数组中的下标取出旧数组中对应的元素,很显然,这里是从后往前倒着来的。

因为我们的 intsetResize 方法已经完成了扩容内存的操作,也就是说新元素的内存已经分配完毕,那么 _intsetSet 方法就会将 _intsetGetEncoded 取出的元素重新的向数组中赋值。循环结束时,就是所有元素重新归位的时候,最后再将新元素赋值进入数组最后的位置。

但其实细心的同学会发现,_intsetSet 方法在传下标索引的时候实际传的是 length+prepend,这其实就是我们说,如果 value 是小于零的,length+prepend 最终会导致所有的旧元素往后挪了一个偏移量,然后新的元素会被赋值的索引为零的位置。也就是说,如果新插入的数值是负数,它会被头插进数组的第一个位置。

核心的几个 API 我们都已经介绍了,其他的一些 API 你可以自行参阅源码,相信对你不难。

总结一下,整数集合(intset)使用了非常简洁的数据结构,可以更少的占用内存存储一些整数,但终究是基于数组的,也就避免不了不能存储大量数据的缺点。总体来说,插入一个元素,最好情况 O(logN),最坏的情况是 O(N),摊还时间复杂度为 O(N),查找一个元素,根据索引下标时间复杂度在 O(1)。当 intset 中的元素超过 512 个,或者向其中添加了字符串,redis 会将 intset 转换成字典。

同样的,如果觉得我写的对你有点帮助的话,顺手点一波关注吧,也欢迎加作者微信深入探讨,我们下一讲,压缩列表,尽请关注。


关注公众不迷路,一个爱分享的程序员。

公众号回复「1024」加作者微信一起探讨学习!

每篇文章用到的所有案例代码素材都会上传我个人 github

https://github.com/SingleYam/overview_java

欢迎来踩!

Redis 的底层数据结构(整数集合)的更多相关文章

  1. redis 底层数据结构 整数集合intset

    整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时Redis就会使用整数集合作为集合键的底层实现 整数集合是Redis用于保存整数值的集合抽象数据结构,它可以保存 ...

  2. Redis数据结构—整数集合与压缩列表

    目录 Redis数据结构-整数集合与压缩列表 整数集合的实现 整数集合的升级 整数集合不支持降级 压缩列表的构成 压缩列表节点的构成 小结 Redis数据结构-整数集合与压缩列表 大家好,我是白泽.今 ...

  3. Redis原理再学习05:数据结构-整数集合intset

    intset介绍 intset 整数集合,当一个集合只有整数元素,且元素数量不多时,Redis 就会用整数集合作为集合键的底层实现. redis> SADD numbers 1 3 5 7 9 ...

  4. Redis(二)--- Redis的底层数据结构

    1.Redis的数据结构 Redis 的底层数据结构包含简单的动态字符串(SDS).链表.字典.压缩列表.整数集合等等:五大数据类型(数据对象)都是由一种或几种数结构构成. 在命令行中可以使用 OBJ ...

  5. 深入理解Redis:底层数据结构

    简介 redis[1]是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合).zset(sorte ...

  6. Redis详解(四)------ redis的底层数据结构

    上一篇博客我们介绍了 redis的五大数据类型详细用法,但是在 Redis 中,这几种数据类型底层是由什么数据结构构造的呢?本篇博客我们就来详细介绍Redis中五大数据类型的底层实现. 1.演示数据类 ...

  7. 图解Redis之数据结构篇——整数集合

    前言     整数集合(intset)并不是一个基础的数据结构,而是Redis自己设计的一种存储结构,是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时, Redis i ...

  8. Redis 的底层数据结构(对象)

    目前为止,我们介绍了 redis 中非常典型的五种数据结构,从 SDS 到 压缩列表,这都是 redis 最底层.最常用的数据结构,相信你也掌握的不错. 但 redis 实际存储键值对的时候,是基于对 ...

  9. Redis学习之intset整数集合源码分析

    1.整数集合:整数的集合,升序排序,无重复元素 2.整数集合intset是集合键的底层实现之一,当一个集合只包含整数值的元素,并且这个集合的元素数量不多时,redis会使用整数集合作为集合键的底层实现 ...

随机推荐

  1. 剑指offer(五):用两个栈实现一个队列

    题目: 用两个栈来实现一个队列,完成队列的Push和Pop操作. 队列中的元素为int类型. 解决办法: 队列先进先出,栈先进后出(stack1和stack2) 其实主要要注意的点是: ①在添加时直接 ...

  2. java反射原理,应用

    java类的加载过程 调用java命令运行程序时,该命令会启动一条java虚拟机进程,该程序的所有线程都会运行在这个虚拟机进程里面.程序运行产生的线程.变量都处于这个进程,共同使用该JVM进程的内存区 ...

  3. android 滚动时间选择器

    一.概述 滚动时间选择现在貌似很常用,所以就总结一下,显示效果一般般 , 做个参考吧! 以上就是效果图,可以滚动选择 日期时间, 由于是在 5.0系统运行的,貌似5.0系统做了什么变动,下面的 &qu ...

  4. Zookeeper系列一:Zookeeper基础命令操作

    有些事不是努力就可以改变的,五十块的人民币设计的再好看,也没有一百块的招人喜欢. 前言 由于公司年底要更换办公地点,所以最近投了一下简历,发现面试官现在很喜欢问dubbo.zookeeper和高并发等 ...

  5. Hessian 接口使用示例总结(转载)

    一.使用hessian接口准备 首先,hessian接口的使用,必须要准备hessian接口的jar包,本文使用的jar包如下:hessian-4.0.7.jar; Hessian接口的使用一般是在两 ...

  6. 6.Sentinel源码分析—Sentinel是如何动态加载配置限流的?

    Sentinel源码解析系列: 1.Sentinel源码分析-FlowRuleManager加载规则做了什么? 2. Sentinel源码分析-Sentinel是如何进行流量统计的? 3. Senti ...

  7. [Spark] 04 - HBase

    BHase基本知识 基本概念 自我介绍 HBase是一个分布式的.面向列的开源数据库,该技术来源于 Fay Chang 所撰写的Google论文“Bigtable:一个结构化数据的分布式存储系统”. ...

  8. 使用$.getJSON()需要注意的地方

    第一 JSON文件里面不能有任何注释,不能使用单引号,必须使用双引号: 第二 JSON文件名不能使用特殊字符 -  ,比如 test-a.json 否则不会返回任何数据也不会报错. 使用方法: $.g ...

  9. 【译】Kubernetes监控实践(2):可行监控方案之Prometheus和Sensu

    本文介绍两个可行的K8s监控方案:Prometheus和Sensu.两个方案都能全面提供系统级的监控数据,帮助开发人员跟踪K8s关键组件的性能.定位故障.接收预警. 拓展阅读:Kubernetes监控 ...

  10. python,json解析字符串时ValueError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

    今天写测试工具的时候,去excel取数据,用json解析字符串为字典时报错,后经调试,发现是单引号的原因,将单引号换位双引号即可 def getExcelValue_to_dic(filepath): ...