最近,我想通过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和数据元素。