携手创作,共同成长!这是我参与「掘金日新计划 · 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为单向链表.至于为什么不是双向链表?理由如下:
- 从双向链表中的任意一个结点开始,都可以很方便地访问前驱结点和后继结点。但是在哈希表中我们都是先查找到某一个元素再对其进行相关操作,既然已经找到该元素了,那么其前后节点都是已知.
- 双向链表适用于从后向前遍历,但是哈希表中不需要该操作.
初始化
类比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;
}
- 当数组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来讲,它是为了将内存的数据进行持久化进行的操作.
扩容条件
触发扩容的条件如下:
- 哈希桶中已有元素个数大于等于数组size.
- 如果有子进程在进行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;
}
- 基于原有元素个数,计算一个大于其值的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;
}
}
- 初始化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;
}