Redis源码分析:字典实现(-)

本文分析的是Redis的版本为4.0.11。

字典(dictionary)

键值数据库(key-value store)是字典的数据结构,键(key)和值(value)进行关联,形成键值对,所有数据库的操作都是通过主键(primary key)来实现的。除了用作表示数据库外,字典还是Redis底层哈希键的底层实现。

字典结构定义

字典结构定义位于dict.h文件中。先来看一下字典结构的定义:

/* Unused arguments generate annoying warnings... */
#define DICT_NOTUSED(V) ((void) V)

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

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);
    void *(*keyDup)(void *privdata, const void *key);
    void *(*valDup)(void *privdata, const void *obj);
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    void (*keyDestructor)(void *privdata, void *key);
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

/* 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;
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

字典的实现,采用了哈希表(hash table)的结构。在dict的结构中,包含了哈希表的结构dictht的数组,数组的长度为2。每个字典包含两个哈希表结构,主要是为了实现增量重新哈希功能时,避免在大数据量的情况下,哈希表一次性扩展时导致的性能问题。
底层哈希表的实现,是典型的数组+拉链的形式,dictht结构中的成员table指向的dictEntry指针数组,size表示哈希表的大小,sizemask为大小掩码,used的表示哈希表中已实现的哈希桶的个数。dict提供下列的宏:

#define dictSlots(d) ((d)->ht[0].size+(d)->ht[1].size)
#define dictSize(d) ((d)->ht[0].used+(d)->ht[1].used)

dictEntry为字典的条目,用于存储key,value及链表指针。value使用了union的结构,方便都不同类型的值进行存储。dictEntry提供下列宏来便捷的访问和操作成员变量:

#define dictSetSignedIntegerVal(entry, _val_) \
    do { (entry)->v.s64 = _val_; } while(0)

#define dictSetUnsignedIntegerVal(entry, _val_) \
    do { (entry)->v.u64 = _val_; } while(0)

#define dictSetDoubleVal(entry, _val_) \
    do { (entry)->v.d = _val_; } while(0)
    
#define dictGetKey(he) ((he)->key)
#define dictGetVal(he) ((he)->v.val)
#define dictGetSignedIntegerVal(he) ((he)->v.s64)
#define dictGetUnsignedIntegerVal(he) ((he)->v.u64)
#define dictGetDoubleVal(he) ((he)->v.d)

字典结构中dictType成员,可以对字典的操作进行定制,注册相关的函数接口;privdata存储了定制接口的参数。

#define dictFreeVal(d, entry) \
   if ((d)->type->valDestructor) \
       (d)->type->valDestructor((d)->privdata, (entry)->v.val)

#define dictSetVal(d, entry, _val_) do { \
   if ((d)->type->valDup) \
       (entry)->v.val = (d)->type->valDup((d)->privdata, _val_); \
   else \
       (entry)->v.val = (_val_); \
} while(0)

#define dictFreeKey(d, entry) \
   if ((d)->type->keyDestructor) \
       (d)->type->keyDestructor((d)->privdata, (entry)->key)

#define dictSetKey(d, entry, _key_) do { \
   if ((d)->type->keyDup) \
       (entry)->key = (d)->type->keyDup((d)->privdata, _key_); \
   else \
       (entry)->key = (_key_); \
} while(0)

#define dictCompareKeys(d, key1, key2) \
   (((d)->type->keyCompare) ? \
       (d)->type->keyCompare((d)->privdata, key1, key2) : \
       (key1) == (key2))
       
#define dictHashKey(d, key) (d)->type->hashFunction(key)

dict结构有一个重要的成员变量rehashidx,用于标示是否正在进行重新哈希过程,若rehashidx=-1,表示没有进行重新过程,涉及到具体函数时会讲解该值在重新哈希中如何变化。头文件中定义了下列宏来是否处于重新哈希过程中:

#define dictIsRehashing(d) ((d)->rehashidx != -1)

字典结构的函数实现

本节将按照字典操作的类型,来对提供对接口实现进行分析。字典的实现在dict.c文件中。

创建操作

创建接口dictCreate函数输入为定制操作接口的dictType类型变量和相关的数据指针,输出为一个dict类型的指针。

dict *dictCreate(dictType *type, void *privDataPtr);

具体实现比较简单,分配dict的内存空间,对哈希表结构和其它成员变量进行初始化。需要注意的是,dictCreate函数创建的dict字典结构还不能直接使用,因为此时的哈希表的table成员为NULL,需要调用dictResize()进行空间分配后才能使用。

/* Reset a hash table already initialized with ht_init().
* NOTE: This function should only be called by ht_destroy(). */
static void _dictReset(dictht *ht)
{
   ht->table = NULL;
   ht->size = 0;
   ht->sizemask = 0;
   ht->used = 0;
}

/* Create a new hash table */
dict *dictCreate(dictType *type,
       void *privDataPtr)
{
   dict *d = zmalloc(sizeof(*d));

   _dictInit(d,type,privDataPtr);
   return d;
}

/* Initialize the hash table */
int _dictInit(dict *d, dictType *type,
       void *privDataPtr)
{
   _dictReset(&d->ht[0]);
   _dictReset(&d->ht[1]);
   d->type = type;
   d->privdata = privDataPtr;
   d->rehashidx = -1;
   d->iterators = 0;
   return DICT_OK;
}

删除操作

字典实现上提供了2个清除字典接口的操作,差异是dictEmpty()提供了一个回调函数的参数:

/* Clear & Release the hash table */
void dictRelease(dict *d);

void dictEmpty(dict *d, void(callback)(void*));

接口的具体实现如下,完成资源的回收工作:

/* Destroy an entire dictionary */
int _dictClear(dict *d, dictht *ht, void(callback)(void *)) {
    unsigned long i;

    /* Free all the elements */
    for (i = 0; i < ht->size && ht->used > 0; i++) {
        dictEntry *he, *nextHe;

        if (callback && (i & 65535) == 0) callback(d->privdata);

        if ((he = ht->table[i]) == NULL) continue;
        while(he) {
            nextHe = he->next;
            dictFreeKey(d, he);
            dictFreeVal(d, he);
            zfree(he);
            ht->used--;
            he = nextHe;
        }
    }
    /* Free the table and the allocated cache structure */
    zfree(ht->table);
    /* Re-initialize the table */
    _dictReset(ht);
    return DICT_OK; /* never fails */
}

/* Clear & Release the hash table */
void dictRelease(dict *d)
{
    _dictClear(d,&d->ht[0],NULL);
    _dictClear(d,&d->ht[1],NULL);
    zfree(d);
}

void dictEmpty(dict *d, void(callback)(void*)) {
    _dictClear(d,&d->ht[0],callback);
    _dictClear(d,&d->ht[1],callback);
    d->rehashidx = -1;
    d->iterators = 0;
}

有点疑问的是,为何在满足(i & 65535) == 0时,_dictClear才调用回掉函数???

哈希表扩展

如果对哈希表进行扩展,那需要提一系列的问题:

  1. 如何选择扩展后哈希表的大小?
    新建哈希表的大小为2的幂次方,大小为大于当前哈希表大小的最小的2的幂次方的值,具体值通过函数_dictNextPower()来计算。
  2. 什么时间进行哈希表的扩展与收缩操作?
    当以下条件的任意一个被满足时,程序会自动对哈希表进行扩展操作:
  • 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1.
  • 服务器目前在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5.
/* 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);
    }
    return DICT_OK;
}
  1. 如何进行新旧哈希表的迁移工作?
    这个问题会在分析dictRehash()函数时回答。

下面的代码为哈希表扩展的相关代码,需要注意的地方:

  1. 哈希表的最小的大小为DICT_HT_INITIAL_SIZE,其值为4.
  2. 如果时第一次初始化,并不涉及重新哈希的过程,只需将创建的哈希表复制给d->ht[0]即可;否则,需要启动重新哈希的过程,将rehashidx设置为0.
/* Resize the table to the minimal size that contains all the elements,
* but with the invariant of a USED/BUCKETS ratio near to <= 1 */
int dictResize(dict *d)
{
   int minimal;

   if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
   minimal = d->ht[0].used;
   if (minimal < DICT_HT_INITIAL_SIZE)
       minimal = DICT_HT_INITIAL_SIZE;
   return dictExpand(d, minimal);
}

/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
   dictht n; /* the new hash table */
   unsigned long realsize = _dictNextPower(size);

   /* the size is invalid if it is smaller than the number of
    * elements already inside the hash table */
   if (dictIsRehashing(d) || d->ht[0].used > size)
       return DICT_ERR;

   /* Rehashing to the same table size is not useful. */
   if (realsize == d->ht[0].size) return DICT_ERR;

   /* Allocate the new hash table and initialize all pointers to NULL */
   n.size = realsize;
   n.sizemask = realsize-1;
   n.table = zcalloc(realsize*sizeof(dictEntry*));
   n.used = 0;

   /* Is this the first initialization? If so it's not really a rehashing
    * we just set the first hash table so that it can accept keys. */
   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;
}

/* 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 + 1LU;
   while(1) {
       if (i >= size)
           return i;
       i *= 2;
   }
}

哈希表重新哈希的过程,实际上就是将旧表数据迁移到新表的过程,即将旧表中某位置的数据从旧表的链表中摘掉,根据key计算新表中的哈希位置,并挂载的对应的新表链表结构中。rehashidx记录旧表中已做迁移处理的索引位置。
为了避免重新哈希时,阻塞太长时间,因此需要在函数输入参数中指定本次最多重新哈希的漏桶个数,同时,会限制处理的空桶的数量。

/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
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;

       /* Note that rehashidx can't overflow as we are sure there are more
        * elements because ht[0].used != 0 */
       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;
}

在上面函数的基础上,内部提供了指定重哈希时间的接口:

long long timeInMilliseconds(void) {
   struct timeval tv;

   gettimeofday(&tv,NULL);
   return (((long long)tv.tv_sec)*1000)+(tv.tv_usec/1000);
}

/* Rehash for an amount of time between ms milliseconds and ms+1 milliseconds */
int dictRehashMilliseconds(dict *d, int ms) {
   long long start = timeInMilliseconds();
   int rehashes = 0;

   while(dictRehash(d,100)) {
       rehashes += 100;
       if (timeInMilliseconds()-start > ms) break;
   }
   return rehashes;
}

你可能感兴趣的:(NoSQL)