redis基础数据结构——SDS与IntSet

1.SDS(简单Simple动态Dynamic字符串String)

在Redis中,key往往都是字符串、value往往都是字符串或者字符串的集合,因此Redis中非常需要有一个字符串的数据结构。

为什么Redis没有直接使用C语言中的字符串数据结构?

  • 获取字符串的长度需要经过计算
  • 非二进制安全,遇到 '\0' 会强制结束
  • 字符串变量会被放入常量池,不能被修改
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 当前字符串长度 */
    uint8_t alloc; /* 当前字符串申请的长度 */
    unsigned char flags; /* SDS头类型 */
    char buf[];
};

上述是一个SDS的最常用结构体:sdshdr8

他是如何解决C语言中字符串的三个问题的

  1. 获取字符串的长度直接通过len获取,时间复杂度被降到了O(1)
  2. 二进制安全,如何判断当前字符串是否结束?只需要看读取的长度和len是否相等即可
  3. 字符串具备动态扩容能力:例如我们在追加一段字符串时,会申请新的空间,
    • 并且如果新的字符串大小小于1MB,则新空间为原空间的2N+1(1是为了放'\0')
    • 如果新的字符串大小超过1MB,则新的空间为原空间的N+1MB+1。这是为了内存预分配
      • 为什么要进行内存预分配:分配内存需要用户态进程向内核态申请,内核态再和操作系统交互获取内存,会消耗大量资源,这么做有利于减少资源的开销

2.IntSet(Redis中一种set的实现方式,基于整数数组实现,具有有序性、唯一性、长度可变性。适用于数据量较小的场景)注:当数据量变大时,查找效率会成为瓶颈,并且由于有序性需要申请连续的空间,数据量过大也会导致这个操作变的更加困难

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

encoding即编码方式,支持存放16位(类似于short) 32位(类似于int) 64位(类似于long)整数

length即当前数组的长度大小

contents用于存放数组

 

使用案例与源码分析:

为了方便查找,IntSet会把数据以连续的地址空间,升序的存入contents。

例如当我们存入1,2,3,4,5时:

  • encoding即为INTSET_ENC_INT16;占4字节
  • length为5;占4字节
  • contents为1,2,3,4,5。占5x2字节。此外他们的地址也是连续的,假设1的地址为0x001,那么2的地址就是0x003。即遵循寻址公式add=startPtr+(sizeof(encoding)*index)

在上述例子中,如果我们插入了50000这个元素,会导致encoding不能满足当前条件,此时就会触发IntSet升级编码:

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    //获取当前inset的编码
    uint8_t curenc = intrev32ifbe(is->encoding);
    //获取新插入值所需要的编码
    uint8_t newenc = _intsetValueEncoding(value);
    //获取当前元素个数
    int length = intrev32ifbe(is->length);
    //判断当前插入的值是大于0还是小于0,因为已经发生扩容了所以插入的肯定是边界值,需要判断插入的是数组头还是数组尾
    //如果小于0,按照升序那么就应该插入到数组头;反之数组尾
    int prepend = value < 0 ? 1 : 0;
    //重置intset的编码与重置数组大小,
    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is,intrev32ifbe(is->length)+1);
    //倒序遍历inset,逐个搬运元素到正确位置,倒序保证了搬运过程中旧元素不会被新元素覆盖而导致元素错乱的问题
    //intsetGetEncoded是按照旧编码查找元素位置,intsetSet是按照新编码放置元素
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
    //根据prepend判断是将元素放在数组头还是尾
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    //修改数组长度
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

intset是如何插入的?总的来说就是先根据当前插入的元素的enc判断是否需要升级;升级则直接在升级内实现插入。不需要升级则先通过二分查找判断当前元素应在的位置(pos)。如果元素重复那么直接返回插入失败;如果元素不重复那就将所有pos+1的地址的元素后移,将新元素插入后修改结构体内的长度信息

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    //获取当前value的encoding
    uint8_t valenc = _intsetValueEncoding(value);
    //当前value的插入位置
    uint32_t pos;
    //判断插入是否成功的标志
    if (success) *success = 1;
    //判断encoding是否需要升级,valenc是否超出当前范围
    if (valenc > intrev32ifbe(is->encoding)) {
        /* This always succeeds, so we don't need to curry *success. */
        return intsetUpgradeAndAdd(is,value);
    } else {
        //元素在set里被查到了,那就直接返回false插入失败
        //这里是用的二分查找,定位pos同时看看set里是不是已经有了具体值,如果有那就说插入失败即可
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }
        //数组扩容,为保证有序性,给新元素腾出对应位置:当前插入位置是pos,即需要移动pos+1的所有元素位置
        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;
}

二分查找的具体实现:

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 */
    //intset如果是空的,那么不需要查找直接将元素放入即可
    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. */
         //边界值判断,看看他是不是最大/最小值,这里在前面已经判断过enc是否兼容了,故而直接插入即可
        if (value > _intsetGet(is,max)) {
            if (pos) *pos = intrev32ifbe(is->length);
            return 0;
        } else if (value < _intsetGet(is,0)) {
            if (pos) *pos = 0;
            return 0;
        }
    }
    //标准的二分查找,通过>>位运算提高效率,其实就是(min+max)/2
    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;
        }
    }
    //找到具体位置后,判断当前元素是否存在,存在则直接返回1,告诉前面别插入了;不存在则返回0,同时更新pos
    if (value == cur) {
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
}

 

posted @ 2025-02-28 17:08  天启A  阅读(38)  评论(0)    收藏  举报