Redis底层数据结构

130 阅读4分钟

sds(简单动态字符串)

struct sdshdr {
    unsigned int len;  //分配到长度
    unsigned int free; //未使用的长度
    char buf[];        //数据
};
  • 扩容
    • 如果free的长度够用直接使用
    • 如果新串的长度小于SDS_MAX_PREALLOC(1M),扩容后的长度为新串长度的两倍
    • 否则扩容后的长度为新串长度加上SDS_MAX_PREALLOC(1M)
sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;
    size_t free = sdsavail(s);
    size_t len, newlen;

    if (free >= addlen) return s;
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
    if (newsh == NULL) return NULL;

    newsh->free = newlen - len;
    return newsh->buf;
}
  • 与c字符串相比的优点
    • 常数复杂度获取字符串的长度
    • 杜绝缓冲区溢出
    • 空间预分配减少修改字符串长度内存重新分配的次数
    • 二进制安全

dict(字典)

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;       //哈希表
    unsigned long size;      //大小
    unsigned long sizemask;  //size-1 用于计算索引
    unsigned long used;      //哈希表已有的节点数
} dictht;

哈希冲突

  • 解决冲突使用的是链地址法,即出现冲突会将entry放在冲突位置的单链表尾部。 扩容
  • 当数据过多哈希表长度又较小时,就会出现很多冲突,导致单链表过长,哈希表的查询性能会急剧下降,故应该在合适的时间对哈希表进行扩容。
  • 扩容条件
    • 负载因子:哈希表中entry的数目 / 哈希表的长度
    • Redis没有正在执行和Fork相关指令时(BGSAVE,BGREWRITEAOF),负载因子大于等于1扩容。
    • Redis正在执行和Fork相关指令时(BGSAVE,BGREWRITEAOF),负载因子大于等于5扩容。 为什么不同条件条件扩容的负载因子不一样?
    • BGSAVE和BGREWRITEAOF会Fork出当前服务器进程的子进程,并且大多数操作系统采用写时复制技术(copy-on——write)来优化Fork一个子进程的时间并且节约内存,所以在子进程运行期间,提高负载因子,尽可能减少父进程对内存的写入操作,尽可能节约内存。
    • 写时复制通俗理解,未采用写时复制技术时,创建一个进程的副本(Fork出一个子进程)会将父进程的内存空间也创建出一个副本给子进程使用,但是这样会浪费内存并且如果父进程空间大Fork的时间也会很长,而采用写时复制技术,Fork一个子进程时不需要创建一个父进程空间的副本,而是子进程和父进程共享内存空间,只有父进程执行写操作时,才会将父进程修改那一页创建一个副本给子进程使用。
  • 扩容后的大小
    • 扩容后的大小为2^n,2^n为第一个大于等于当前哈希表entry数目*2的数
/* Our hash table capability is a power of two */
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE;

    if (size >= LONG_MAX) return LONG_MAX;
    //找到第一个大于等于size的2^n作为扩容后的大小,size=d->ht[0].used*2
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;

    /* If the hash table is empty expand it to the initial size. */
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
        return dictExpand(d, d->ht[0].used*2); //传过去的size为当前哈希表元素的二倍
    }
    return DICT_OK;
}
  • 缩容
    • 负载因子小于0.1进行缩容
    • 缩容后的大小为2^n,2^n为第一个大于等于当前哈希表entry数目*2的数 渐进式rehash
  • 开始时将rehashidx设置为0,表示rehash正式开始。
  • 在rehash期间,每次对字典进行增删改查操作时,除了执行指定操作外,还会将ht[0]的rehashidx桶位上的entry,rehash到ht[1],完成后rehash后,rehashidx加1。
  • rehash完成后rehashidx重新设置为-1。 --------待续--------

list(双端列表)

intset(整数集合)

ziplist(压缩列表)

skiplist(跳跃表)