String底层实现——动态字符串SDSredis
面试官:说说Redis的Hash底层 我:......(来自阅文的面试题)bash
跳跃表肯定不了解下😏
数据结构
大噶好,今天仍然是元气满满的一天,抛开永远写不完的需求,拒绝要求贼变态的客户,单纯的学习技术,感觉技术的魅力。(哈哈哈,皮一下很开森)源码分析
前面几周咱们一块儿看了Redis底层数据结构,如动态字符串SDS
,双向链表Adlist
,字典Dict
,跳跃表
,若是有对Redis常见的类型或底层数据结构不明白的请看上面传送门。post
今天来讲下set的底层实现整数集合
,若是有对set不明白的,常见的API使用这篇就不讲了,看上面的传送门哈。学习
整数集合是Redis设计的一种底层结构,是set的底层实现,当集合中只包含整数值元素,而且这个集合元素数据很少时,会使用这种结构。可是若是不知足刚才的条件,会使用其余结构,这边暂时不讲哈。ui
下图为整数集合的实际组成,包括三个部分,分别是编码格式encoding,包含元素数量length,保存元素的数组contents。(这边只须要简单看下,下面针对每一个模块详细说明哈😝)编码
咱们看下intset.h里面关于整数集合的定义,上代码哈:
//整数集合结构体
typedef struct intset {
uint32_t encoding; //编码格式,有以下三种格式,初始值默认为INTSET_ENC_INT16
uint32_t length; //集合元素数量
int8_t contents[]; //保存元素的数组,元素类型并不必定是ini8_t类型,柔性数组不占intset结构体大小,而且数组中的元素从小到大排列。
} intset;
#define INTSET_ENC_INT16 (sizeof(int16_t)) //16位,2个字节,表示范围-32,768~32,767
#define INTSET_ENC_INT32 (sizeof(int32_t)) //32位,4个字节,表示范围-2,147,483,648~2,147,483,647
#define INTSET_ENC_INT64 (sizeof(int64_t)) //64位,8个字节,表示范围-9,223,372,036,854,775,808~9,223,372,036,854,775,807复制代码
包括INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64三种类型,其分别对应着不一样的范围,具体看上面代码的注释信息。
由于插入的数据的大小是不同的,为了尽量的节约内存
(毕竟都是钱,平时要省着点用😭),因此咱们须要使用不一样的类型来存储数据。
记录了保存数据contents的长度,即有多少个元素。
真正存储数据的地方,数组是按照从小到大
有序排序的,而且不包含任何重复项
(由于set是不含重复项,因此其底层实现也是不含包含项的)。
上面的图咱们从新看下,编码格式encoding为INTSET_ENC_INT16,即每一个数据占16位。长度length为4,即数组content里面有四个元素,分别是1,2,3,4。若是咱们要添加一个数字位40000,很明显超过编码格式为INTSET_ENC_INT16的范围-32,768~32,767,应该是编码格式为INTSET_ENC_INT32。那么他是如何升级的呢,从INTSET_ENC_INT16升级到INTSET_ENC_INT32的呢?
首先咱们看下1,2,3,4这四个元素是如何存储的。首先要知道一共有多少位,计算规则为length*编码格式的位数
,即4*16=64
。因此每一个元素占用了16位。
新的元素为40000,已经超过了INTSET_ENC_INT16的范围-32,768~32,767,因此新的编码格式为INTSET_ENC_INT32。
上面已经说明了编码格式为INTSET_ENC_INT32,计算规则为length*编码格式的位数
,即5*32=160
。因此新增的位数为64-159。
从上面知道按照新的编码格式,每一个数据应该占用32位,可是旧的编码格式,每一个数据占用16位。因此咱们从后面开始,每次获取32位用来存储数据。
这样说太难懂了,看下图☺。
首先,那最后32位,即128-159存储40000。那么第49-127是空着的。
接着,取空着的49-127最后的32位,即96到127这32位,用来存储4。那么以前4存储的位置48-63
和49-127剩下的64-95
这两部分组成了一个大部分,即48-95
,如今空着啦。
在接着在48-95这个大部分,再取后32位,即64-95,用来存储3。那么以前3存储位置32-47
和48-95剩下的48-63
这两部分组成了一个大部分,即32-63
,如今空着啦。
再接着,将32-63这个大部分,再取后32位,即仍是32-63,用来存储2。那么以前2存储位置16-31空着啦。
最后,将16-31和原来0-31合起来,存储1。
至此,整个升级过程结束。总体来讲,分为3步,肯定新的编码格式,新增须要的内存空间,从后往前调整数据。
这边有个小问题,为啥要从后往前调整数据呢?
缘由是若是从前日后,数据可能会覆盖。也拿上面个例子来讲,数据1在0-15位,数据2在16-31位,若是从前日后,咱们知道新的编码格式INTSET_ENC_INT32要求每一个元素占用32位,那么数据1应该占用0-31,这个时候数据2就被覆盖了,之后就不知道数据2啦。
可是从后往前,由于后面新增了一些内存,因此不会发生覆盖现象。
整数集合既可让集合保存三种不一样类型的值,又能够确保升级操做只在有须要的时候进行,这样就节省了内存。
一旦对数组进行升级,编码就会一直保存升级后的状态。即便后面把40000删掉了,编码格式仍是不会将会INTSET_ENC_INT16。
这个方法比较简单,是初始化整数集合的步骤,即下图部分。
主要的步骤是分配内存空间,设置默认编码格式,以及初始化数组长度length。
intset *intsetNew(void) {
intset *is = zmalloc(sizeof(intset));//分配内存空间
is->encoding = intrev32ifbe(INTSET_ENC_INT16);//设置默认编码格式INTSET_ENC_INT16
is->length = 0;//初始化length
return is;
}
复制代码
能够根据上面的流程图,对照着下面的源码分析,这边就不写啦哈。
//添加元素
//输入参数*is为原整数集合
//value为要添加的元素
//*success为是否添加成功的标志量 ,1表示成功,0表示失败
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
//肯定要添加的元素的编码格式
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
//若是success没有初始值,则初始化为1
if (success) *success = 1;
//若是新的编码格式大于如今的编码格式,则升级并添加元素
if (valenc > intrev32ifbe(is->encoding)) {
//调用另外一个方法
return intsetUpgradeAndAdd(is,value);
} else {
//若是编码格式不变,则调用查询方法
//输入参数is为原整数集合
//value为要添加的数据
//pos为位置
if (intsetSearch(is,value,&pos)) {//若是找到了,则直接返回,由于数据是不可重复的。
if (success) *success = 0;
return is;
}
//设置length
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;
}
//#define INT8_MAX 127
//#define INT16_MAX 32767
//#define INT32_MAX 2147483647
//#define INT64_MAX 9223372036854775807LL
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;
}
//根据输入参数value的编码格式,对整数集合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);
//若是要添加的数据小于0,则prepend为1,不然为0
int prepend = value < 0 ? 1 : 0;
//设置集合为新的编码格式,并根据编码格式从新设置内存
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1);
//逐步循环,直到length小于0,挨个从新设置每一个值,从后往前
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
//若是value为负数,则放在最前面
if (prepend)
_intsetSet(is,0,value);
else//若是value为整数,设置最末尾的元素为value
_intsetSet(is,intrev32ifbe(is->length),value);
//从新设置length
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
//找到is集合中值为value的下标,返回1,并保存在pos中,没有找到返回0,并将pos设置为value能够插入到数组的位置
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;
//若是集合为空,那么位置pos为0
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)) { //若是这个数据小于数组下标为0的数据,即为最小值 ,返回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;//设置参数pos,返回1,即找到位置
return 1;
} else {//若是没找到,则min和max相邻,随便设置都行,并返回0
if (pos) *pos = min;
return 0;
}
}复制代码
该篇主要讲了Redis的SET数据类型的底层实现整数集合,先从整数集合是什么,,剖析了其主要组成部分,进而经过多幅过程图解释了intset是如何升级的,最后结合源码对整数集合进行描述,如建立过程,升级过程,中间穿插例子和过程图。
若是以为写得还行,麻烦给个赞👍,您的承认才是我写做的动力!
若是以为有说的不对的地方,欢迎评论指出。
好了,拜拜咯。
1.若是本文对你有帮助,就点个赞支持下吧,你的「赞」是我创做的动力。
2.关注公众号「学习Java的小姐姐」便可加我好友,我拉你进「Java技术交流群」,你们一块儿共同交流和进步。