最近,我想通过redis的源码来学习redis。虽然平时工作中用得不多,不过对redis还是比较感兴趣的,毕竟它的性能是不错的。redis是一个开源的项目,我们可以通过源代码去了解redis。我后面会通过自己的学习,写一些关于redis源码的帖子。帖子的主要内容是分析代码设计,而并不会对源码进行详细解说。如果有不对的地方,请指正。源码是reids 3.0.3版本。
intset
一、intset数据结构
intset,是用于存储整数集合的数据结构。set的特点是无重复元素,元素可以无序。不过redis的intset是一个元素有序的数据结构。
先看intset的定义:
typedef struct intset { uint32_t encoding; //整型编码类型 uint32_t length; //intset大小,元素个数 int8_t contents[]; //数据存储区 } intset;
先结合intset的相关操作,再来谈intset的特点。下面通过部分代码来说明intset的行为特点。
二、inetset提供的相关操作函数
intset对外暴露的函数:
intset *intsetNew(void); //创建一个空的intset 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); //随机选取元素 uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value); //获取指定位置的元素 uint32_t intsetLen(intset *is); //获取intset元素个数 size_t intsetBlobLen(intset *is); //获取intset所用空间大小
1. 整型编码
/* 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)) /* Return the required encoding for the provided value. */ 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; }
支持int16_t, int32_t, int64_t 四种类型的存储。每种类型的值为其空间大小,这样定义可以满足 INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64,后面类型的值域包含前面的值域。
注:_intsetValueEncoding 函数声明为 static,是文件内部的函数,外部不可见。
2. 获取指定位置的元素
/* Return the value at pos, given an encoding. */ static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) { int64_t v64; int32_t v32; int16_t v16; //按照传入的编码类型去解析数据元素的数组 //intset是按小端进行存储的,所以还有可能对获取的元素进行大端转小端的转换 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; } } /* Return the value at pos, using the configured encoding. */ static int64_t _intsetGet(intset *is, int pos) { //把intset记录的编码类型传入函数 return _intsetGetEncoded(is,pos,intrev32ifbe(is->encoding)); }
虽然intset内部对整数进行编码存储,对外暴露的都是int64_t类型。
内部通过对数据进行编码存储,有可能节省存储空间。
因为intset是小端存储的,所以读取或写入数据时,都需要进行大小端转换。但目前我还没有明白这样做的必要,因为觉得intset只是内部的数据结构,可以不用硬性要求用大小端存储。
3. 插入元素(可能引起intset重组)
/* 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)) { //如果value的编码类型比当前intset的编码类型要大,元素一定不在intset中, //需要插入,同时需要扩展intset的编码类型。升级intset的编码类型时, //动态申请更大的空间以足够存储扩展后的length+1个元素, //具体参见 intsetUpgradeAndAdd实现 /* 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; }
需要注意的是,如果插入的数据的编码类型比当前intset的编码类型要大,为了能存储新元素,intset需要进行类型扩展。扩展类型时会把intset中原有的所有元素都扩展成新的类型,这样就需要重组intset,当原来元素个数比较多重组会稍费时。不过intset最多会进行两次重组。因为有可能进行重组,这也是intset不适合存储大量元素的原因之一。redis中默认配置intset元素个数达512时,就会采用另外的数据结构来存储。
4. 查找元素
/* 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; } } /* Determine whether a value belongs to this set */ uint8_t intsetFind(intset *is, int64_t value) { uint8_t valenc = _intsetValueEncoding(value); //如果value的编码类型大于intset的编码类型,则value一定不存在于intset中 return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL); }
查找的时间复杂度是O(logN)的,对于N比较小时是可以接受的,但当N比较大时,如 1000000时,比较次数就可能达到20次了。
5. 删除元素(不会引起intset重组)
/* 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; }
intset在删除元素时是不会进行元素的数据类型检查,以降级intset的整型类型,intset也不另外提供这样的能力。原因我想大致有:a.需要扫描所有元素以确定目前是否需要进行编码类型降级。b.降级编码类型会引起重组。c.降级后可能需要再进行升级,如果不幸发生频繁升级降级重组,性能不稳定。
结合插入删除函数,都使用了intsetResize,里面中使用了zrealloc来进行内存空间的增减。在增大空间时要么重新分配一块新的空间,要么在原有的空间的最后进行扩展。减小空间时只需在原有的空间最后进行减小即可。这样的方式不太容易产生内存碎片。
三、特点(优缺点需要进行比较才能显出来,由于这里没有比较,优缺点不明显,所以这里只列举了特点):
1.有序数组,存储元素紧密,空间利用率高,而且不容易因频繁地插入删除而产生内存碎片。
2.支持整型编码,intset中所有数据元素的存储类型是一致的。新插入数据时,如果数据的类型大于当前intset的数据类型,则可扩大存储的整型类型(但intset不提供缩小编码类型的能力)。在一定程序上可节省空间,但所有的存取都需要考虑类型,增加代码的复杂度。
3.有长度字段可记录元素个数,取元素个数操作为常量。
4.有序数组,查找元素O(logN)
5.插入元素时,可能需要移动至多N个元素,也有可能使编码类型升级,重组intset。由于这两个特点,intset并不适合于频繁插入和存储大量元素。
6.删除元素时,可能需要移动至多N-1个元素。
7.以小端形式存储数据,包括encoding,length和数据元素。
本文出自 “chhquan” 博客,请务必保留此出处http://chhquan.blog.51cto.com/1346841/1770574
原文地址:http://chhquan.blog.51cto.com/1346841/1770574