Redis数据结构之dictht(二)

212 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情

dictht结构

我们前面提到redis中所有的key都是存放在一个全局哈希表中,既然是哈希表,必然存在hash冲突和扩容.缩容的操作.所以我们今天一起分析下其底层结构

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

首先table作为哈希桶,那么dictEntry必然是一个链表结构,至于是单向链表还是双向链表,结合源码具体分析

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

由图可知dictEntry为单向链表.至于为什么不是双向链表?理由如下:

  1. 从双向链表中的任意一个结点开始,都可以很方便地访问前驱结点和后继结点。但是在哈希表中我们都是先查找到某一个元素再对其进行相关操作,既然已经找到该元素了,那么其前后节点都是已知.
  2. 双向链表适用于从后向前遍历,但是哈希表中不需要该操作.
初始化

类比java中的HashMap,redis中的哈希桶也存在初始化的过程.初始化一定在首次存储元素时进行的(源码位置dict.c).

static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;

  
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio) &&
        dictTypeExpandAllowed(d))
    {
        return dictExpand(d, d->ht[0].used + 1);
    }
    return DICT_OK;
}
  1. 当数组ht[0]未初始化时,进行初始化,长度为DICT_HT_INITIAL_SIZE=4

dict_can_resize默认值为1,那么什么时候修改为0就很关键.那么找到调用dictDisableResize方法的入口即可.

static int dict_can_resize = 1;

static unsigned int dict_force_resize_ratio = 5;


void dictEnableResize(void) {
    dict_can_resize = 1;
}

void dictDisableResize(void) {
    dict_can_resize = 0;
}

server.c中存在updateDictResizePolicy调用,而调用入口在redisFork

int redisFork(int purpose) {
 updateDictResizePolicy();
    return childpid;
}

对于redisFork来讲,它是为了将内存的数据进行持久化进行的操作.

扩容条件

触发扩容的条件如下:

  1. 哈希桶中已有元素个数大于等于数组size.
  2. 如果有子进程在进行RDB或者AOF时,ht[0]的used大于size的5倍的 时候,会触发扩容

因为redis在扩容时基于cow操作写时复制理论进行的,所以也需要额外的内存来存储数据,为了避免出现OOM,所以会出现大于等于size的5倍该操作.

如何扩容

redis的扩容操作代码如下:

int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
    if (malloc_failed) *malloc_failed = 0;

    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    dictht n; 
    unsigned long realsize = _dictNextPower(size);


    if (realsize == d->ht[0].size) return DICT_ERR;
    n.size = realsize;
    n.sizemask = realsize-1;
    if (malloc_failed) {
        n.table = ztrycalloc(realsize*sizeof(dictEntry*));
        *malloc_failed = n.table == NULL;
        if (*malloc_failed)
            return DICT_ERR;
    } else
        n.table = zcalloc(realsize*sizeof(dictEntry*));

    n.used = 0;

    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}
  1. 基于原有元素个数,计算一个大于其值的2的n次幂的值.
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE;

    if (size >= LONG_MAX) return LONG_MAX + 1LU;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}
  1. 初始化ht[1]为扩容后的数据,并重置rehashidx为0,代表数据迁移从第讴歌哈希桶开始.

而数据迁移操作在dictRehash中,首先根据rehashidx是否为-1判断是否在扩容中.接着对ht[0]中某个哈希桶中的元素迁移到ht[1]的哈希桶中,若ht[0]中元素个数为0,则代表迁移完成.

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while(de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}

image.png