redis源码学习-dict篇
0 条评论redis源码学习-dict篇
dict介绍
redis中的dict(字典)实际上是一个安全的实现了rehash的多态哈希桶,是一个性能较高的数据结构,用来保存键值对,类似其他高级语言的map。
dict分析
dict作为一个基于hash提高性能的kv结构,通过开链法/拉链法的方式解决hash冲突。但也因此会存在桶倾斜的问题。优化链表过长的方式有很多,比如java中concurrenthashmap在链表长度达到一定值时将单链表重构成红黑树等。redis解决hash冲突的方式为rehash,通过在声明dict时直接分配**ht_table[2],其中一个ht_table只有在rehash的时候起效,同时辅助设置rehashidx等字段标示是否在rehash状态,实现了安全的rehash。
dict数据结构定义有三个,这里依次分析(6中将typedef struct dictht优化掉了)
dictEntry:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; /* Next entry in the same hash bucket. */
void *metadata[]; /* An arbitrary number of bytes (starting at a
* pointer-aligned address) of size as returned
* by dictType's dictEntryMetadataBytes(). */
} dictEntry;
dictEntry为具体的dict节点结构了,可以看到其中有next指针,指向另一个dictEntry。借助注释中的/* Next entry in the same hash bucket. */可以很好的理解了文章最开始说的开链法/拉链法。哈希桶的结构如下:
先讲几个概念
哈希散列:
若结构中存在和关键字K相等的记录,则必定在f(K)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数(Hash function),按这个事先建立的表为散列表(来自百度)
翻译一下的话,假如有k长度的数组,n个数字,通过一定的规则f将n个数字尽量均匀的放到k个数组中,而在查找某个数字在哪里时,通过规则f直接就能查到在哪里了,无需循环遍历整个数组。其中规则f有很多种方式,有时因地制宜。常见的就是n个数字,k长度的数组,每个数字对k进行取余。又比如有些手机号,那么就取最后四位,身份证号取*****进行取余等等等等。
哈希冲突:
还是以对k取余为例,当两个数字对k取余得到了相同的结果时,就产生了冲突。,举个简单例子,有0 1 2 3 4 5共六个数,有[5]的数组,那么在0-4时会变成
一个数一个坑位。当开始插入5时,发现0已经占了,那么哈希桶的方式怎么办呢?开个链表。0这里不止记录自己,还记录我有next,next那个数是5,如图
所以说,dictEntry中的next就是做这个的。
dictType:
typedef struct dictType {
uint64_t (*hashFunction)(const void *key);
void *(*keyDup)(dict *d, const void *key);
void *(*valDup)(dict *d, const void *obj);
int (*keyCompare)(dict *d, const void *key1, const void *key2);
void (*keyDestructor)(dict *d, void *key);
void (*valDestructor)(dict *d, void *obj);
int (*expandAllowed)(size_t moreMem, double usedRatio);
/* Allow a dictEntry to carry extra caller-defined metadata. The
* extra memory is initialized to 0 when a dictEntry is allocated. */
size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;
看注释的话,这里允许dictEntry携带额外的调用者定义的元数据,而dict自身就依赖此实现多态。这里的方法稍后分析。
dict:
struct dict {
dictType *type;
dictEntry **ht_table[2];
unsigned long ht_used[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
/* Keep small vars at end for optimal (minimal) struct padding */
int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
};
dict最后向外暴露的就是这个了。
可以看到,定义了type,数据为ht_table[2](两个用来做rehash)。ht_used[2]对应的是ht_table[2]中的所用length。rehashidx则为保证安全rehash的标识符,rehashing not in progress if rehashidx == -1 。pauserehash为6后新加入的特性,为保证效率新增rehash的paused的操作。exponent这里为2的多少次方,这个特性也是比较常见的,比如centos的网络中,connection timeout的retry策略就是,第一次1s超时,第二次2s超时,第三次4s超时++++(所以一般机器层面的连接超时大多都是1s,3s,7s,15s++++)
dict方法分析:
create:
/* Reset a hash table already initialized with ht_init().
* NOTE: This function should only be called by ht_destroy(). */
static void _dictReset(dict *d, int htidx)
{
d->ht_table[htidx] = NULL;
d->ht_size_exp[htidx] = -1;
d->ht_used[htidx] = 0;
}
/* Create a new hash table */
dict *dictCreate(dictType *type)
{
dict *d = zmalloc(sizeof(*d));
_dictInit(d,type);
return d;
}
/* Initialize the hash table */
int _dictInit(dict *d, dictType *type)
{
_dictReset(d, 0);
_dictReset(d, 1);
d->type = type;
d->rehashidx = -1;
d->pauserehash = 0;
return DICT_OK;
}
分配堆内存仍然使用的zmalloc.c中的zmalloc。初始化时需要指定dictType,其余的给予默认值,两个dictEntry和所标识的ht_size_exp,ht_used完全一致。
insert:
/* Add an element to the target hash table */
int dictAdd(dict *d, void *key, void *val)
{
dictEntry *entry = dictAddRaw(d,key,NULL);
//调用dictAddRaw后,如果key已经冲突,则返回1
if (!entry) return DICT_ERR;
dictSetVal(d, entry, val);
//key没有被占用,正常add进去的话,则返回0
return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
long index;
dictEntry *entry;
int htidx;
//判断这里是到达了rehashing的条件,如果需要的话则进行rehash,rehash为while循环,需要等得rehash完之后才能进行add
if (dictIsRehashing(d)) _dictRehashStep(d);
/* Get the index of the new element, or -1 if
* the element already exists. */
//如果key存在的话,直接返回
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
/* Allocate the memory and store the new entry.
* Insert the element in top, with the assumption that in a database
* system it is more likely that recently added entries are accessed
* more frequently. */
//查看是否rehash,假如在rehash的话,那么就只在ht_table[1]进行插入,而在select时,则就需要同时在ht_table[0]和ht_table[1]上都查找了
//这么做对于查找来说增加了一定的时间,但是对于内存上有节省,算是以空间换时间的一种形式
htidx = dictIsRehashing(d) ? 1 : 0;
size_t metasize = dictMetadataSize(d);
entry = zmalloc(sizeof(*entry) + metasize);
//调用c库函数,假如value值不为空的话,使用memset赋值给此函数最开始声明的entry
if (metasize > 0) {
memset(dictMetadata(entry), 0, metasize);
}
entry->next = d->ht_table[htidx][index];
d->ht_table[htidx][index] = entry;
d->ht_used[htidx]++;
/* Set the hash entry fields. */
dictSetKey(d, entry, key);
return entry;
}
reset:
int dictReplace(dict *d, void *key, void *val)
{
dictEntry *entry, *existing, auxentry;
/* Try to add the element. If the key
* does not exists dictAdd will succeed. */
//首先尝试调用插入,假如能插入则说明本身没有,直接返回即可
entry = dictAddRaw(d,key,&existing);
if (entry) {
dictSetVal(d, entry, val);
return 1;
}
/* Set the new value and free the old one. Note that it is important
* to do that in this order, as the value may just be exactly the same
* as the previous one. In this context, think to reference counting,
* you want to increment (set), and then decrement (free), and not the
* reverse. */
//如果已有数据,需要替换value,那么就先记录原值的内存地址,然后重新赋值,之后根据记录的地址进行free操作
auxentry = *existing;
dictSetVal(d, existing, val);
dictFreeVal(d, &auxentry);
return 0;
}
delete:
int dictDelete(dict *ht, const void *key) {
return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}
/* Search and remove an element. This is a helper function for
* dictDelete() and dictUnlink(), please check the top comment
* of those functions. */
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
uint64_t h, idx;
dictEntry *he, *prevHe;
int table;
/* dict is empty */
if (dictSize(d) == 0) return NULL;
//判断这里是到达了rehashing的条件,如果需要的话则进行rehash,rehash为while循环,需要等得rehash完之后才能进行add
if (dictIsRehashing(d)) _dictRehashStep(d);
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
//计算hash值
idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
he = d->ht_table[table][idx];
prevHe = NULL;
while(he) {
//假如在桶内循环找到了这个key,那么将他的上一个next指针指向他的下一个,避免丢失入口,然后删除此entry
if (key==he->key || dictCompareKeys(d, key, he->key)) {
/* Unlink the element from the list */
if (prevHe)
prevHe->next = he->next;
else
d->ht_table[table][idx] = he->next;
if (!nofree) {
dictFreeUnlinkedEntry(d, he);
}
d->ht_used[table]--;
return he;
}
prevHe = he;
he = he->next;
}//如果没有在rehash的话,继续delete,如果在rehash的过程中delete的话,那么不会执行删除
if (!dictIsRehashing(d)) break;
}
return NULL; /* not found */
}
search:
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
uint64_t h, idx, table;
if (dictSize(d) == 0) return NULL; /* dict is empty */
if (dictIsRehashing(d)) _dictRehashStep(d);
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
he = d->ht_table[table][idx];
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
if (!dictIsRehashing(d)) return NULL;
}
return NULL;
}
这里和delete的逻辑几乎完全相同,只不过是delet在定位到key后做链表重定向,而find操作就简单一些了,直接返回。所以这里就只放源码不加注释了。
rehash:
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_used[0] != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);
while(d->ht_table[0][d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht_table[0][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) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
de->next = d->ht_table[1][h];
d->ht_table[1][h] = de;
d->ht_used[0]--;
d->ht_used[1]++;
de = nextde;
}
d->ht_table[0][d->rehashidx] = NULL;
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
if (d->ht_used[0] == 0) {
zfree(d->ht_table[0]);
/* Copy the new ht onto the old one */
d->ht_table[0] = d->ht_table[1];
d->ht_used[0] = d->ht_used[1];
d->ht_size_exp[0] = d->ht_size_exp[1];
_dictReset(d, 1);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
对于代码上对外交互较多,需要完整的理解,这里就用文字解释了。
redis的rehash为渐进式的,redis本身不能够保证redis的dict数据量极端值,假如在数据量极大的情况下,rehash所有的kv,那么redis在rehash的过程中将会导致大量的阻塞,导致业务故障,因此redis为保证效率的同时,每次只对dict的**一个桶做rehash。**也因此引入了rehashidx字段,表示现在rehash到了哪个桶上,当到了最后一个桶,那么就rehash完毕,rehashidx变为-1。但也因此,rehash的过程就有可能持续很长时间了。在dict完成全部rehash之前,会有增删改查四个操作。由于不确定此key是否已经被rehash,那么查找时需要对两个ht_table同时查找。插入时,由于redis已经判断现在出现了桶倾斜,因此只插入到ht_table[1]中,而删除与修改和查找一致,都需要从两个ht_table找。rehash的渐进式触发条件为每一次的增删查。在上述解释中可以看到,redis将一个完整的rehash按桶渐进,分解成每一步增删查操作,将一次大的阻塞平摊到所有的操作中,达到高性能优化的目的。
触发rehash的条件:
* the number of elements and the buckets > dict_force_resize_ratio. */
static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;
- 服务器当前没有在执行BG-SAVE或BG-AOF命令且哈希表的负载因子大于等于1时进行扩展操作
- 服务器正在执行BG-SAVE或BG-AOF命令且哈希表的负载因子大于等于5时进行扩展操作
rehash缩容:
/* This is the initial size of every hash table */
#define DICT_HT_INITIAL_SIZE 4
/* Hash table parameters */
#define HASHTABLE_MIN_FILL 10 /* Minimal hash table fill 10% */
#define HASHTABLE_MAX_LOAD_FACTOR 1.618 /* Maximum hash table load factor. */
int htNeedsResize(dict *dict) {
long long size, used;
size = dictSlots(dict);
used = dictSize(dict);
return (size > DICT_HT_INITIAL_SIZE &&
(used*100/size < HASHTABLE_MIN_FILL));
}
如上方法部分来自zset,以zset为例,
当哈希表的大小大于 4,且字典的填充率低于 10时进行缩容