太长不看版java
- redis字典底层使用哈希表实现
- 使用除留余数法进行散列,用到了SipHash算法
- 使用单独链表法解决冲突
- 经过扩张(长度变动为首个>= 2 * used的
)与收缩(长度变动为首个 >= used的
)哈希表维持载荷因子大小合理。(used为目前已有键值对个数)
- 有持久化子进程时因子>=5 扩张,不能收缩。无持久化进程时,因子 >= 1扩张, < 0.1收缩。
- rehash操做是渐进处理的,分散在触发后对当前字典的每一个增删改查操做中。
本篇解析基于redis 5.0.0版本,本篇涉及源码文件为dict.c, dict.h, siphash.c。python
dict全称dictionary,使用键-值(key-value)存储,具备极快的查找速度。常见的高级语言中都有对应的内置数据类型,python中为dict,java/c++中为map。c++
没接触太高级语言?不要紧,往下看,看完本身写一个!git
// 字典定义
typedef struct dict {
// 类型信息 是一个针对某类型的字典操做函数的集合
dictType *type;
// 保存须要传给那些类型特定函数的可选参数,例如复制键/复制值等操做函数
void *privdata;
// 一个长度为2的dict_hast_table数组
dictht ht[2];
// rehash标记
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 键值对个数
unsigned long iterators; /* number of iterators currently running */
} dict;
// 字典类型数据定义
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;
// 哈希表定义
typedef struct dictht {
// dictEntry* 类型数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 始终等于size - 1, 进行散列时有用到
// 为何单独一个字段存储: 只在增删的时候修改,频繁操做下减小计算(读多写少)
unsigned long sizemask;
// 目前已有键值对数量
unsigned long used;
} dictht;
// 哈希节点定义
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 用来解决hash冲突
struct dictEntry *next;
} dictEntry;
复制代码
从上述定义能够看出,redis实现的dict使用哈希表实现。众所周知,影响哈希表查找效率有如下三个因素:github
1.散列函数是否均匀;redis
2.处理冲突的方法;算法
3.散列表的载荷因子(英语:load factor)。编程
不周知的同窗戳维基百科周知一下数组
因而就引出了三个问题:ruby
1.redis的哈希表是如何进行散列?
2.redis的哈希表如何解决冲突?
3.redis是如何保证哈希表的载荷因子处于合理区间?
/* Returns the index of a free slot that can be populated with * an hash entry for the given 'key'. * If the key already exists, -1 is returned. */
static int _dictKeyIndex(dict *ht, const void *key) {
unsigned int h;
dictEntry *he;
/* Expand the hashtable if needed */
if (_dictExpandIfNeeded(ht) == DICT_ERR)
return -1;
/* Compute the key hash value */
// 计算hash值后与sizemask取余得到散列地址
h = dictHashKey(ht, key) & ht->sizemask;
/* Search if this slot does not already contain the given key */
he = ht->table[h];
while(he) {
if (dictCompareHashKeys(ht, key, he->key))
return -1;
he = he->next;
}
return h;
}
复制代码
散列函数通常有6种方法: 直接定址法、数字分析法、平方取中法、折叠法、随机数法、除留余数法。redis内部实现采用了除留余数法。
除留余数法
取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。
除留余数法中的p, redis使用SipHash算法来进行计算,从而减小哈希冲突。值得一提的是python、perl、ruby等编程语言也使用SipHash做为哈希算法。
/* The default hashing function uses SipHash implementation * in siphash.c. */
uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k);
uint64_t siphash_nocase(const uint8_t *in, const size_t inlen, const uint8_t *k);
uint64_t dictGenHashFunction(const void *key, int len) {
return siphash(key,len,dict_hash_function_seed);
}
uint64_t dictGenCaseHashFunction(const unsigned char *buf, int len) {
return siphash_nocase(buf,len,dict_hash_function_seed);
}
复制代码
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) {
long index;
dictEntry *entry;
dictht *ht;
// ...
entry = zmalloc(sizeof(*entry));
// 将新增节点放在冲突链头部,由于是单向链表
entry->next = ht->table[index];
ht->table[index] = entry;
ht->used++;
/* Set the hash entry fields. */
dictSetKey(d, entry, key);
return entry;
}
复制代码
处理哈希冲突方法有: 线性探测法、平方探测法、伪随机探测法、单独链表法、双散列法和再散列法。从上述代码中能够看出,redis采用了单独链表法,在出现冲突时,将新加入节点放在链表头节点(由于是单向链表,获取尾部节点须要O(n)复杂度)。
载荷因子 = 填入表中的元素个数 / 哈希表的长度
考虑如下三种状况:
经过上述分析,咱们能够看到,载荷过高很差,影响效率,过低也很差,内存利用率过低,不划算。最好是始终保持载荷为1,可是显然不现实,因此只能是动态的检测,高了就把哈希表扩张下,低了就把哈希表收缩下,始终将载荷因子维持一个合理的区间。
// 哈希表扩张函数(包含收缩)
int dictExpand(dict *d, unsigned long size)
{
// ...
dictht n; /* the new hash table */
// 实际扩张或缩小后的大小
// 2的次方中第一个大于等于size的数
unsigned long realsize = _dictNextPower(size);
// ...
}
static unsigned long _dictNextPower(unsigned long size) {
unsigned long i = DICT_HT_INITIAL_SIZE;
if (size >= LONG_MAX) return LONG_MAX;
while(1) {
if (i >= size)
return i;
i *= 2;
}
}
复制代码
void updateDictResizePolicy(void) {
// 若是不存在rdb或aof文件变动子进程,resize标记为1
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
// dict_can_resize = 1;
dictEnableResize();
// 不然resize标记为0
else
// dict_can_resize = 0;
dictDisableResize();
}
/* 若是须要进行哈希扩张 */
static int _dictExpandIfNeeded(dict *d)
{
// ...
// 若是已存在键值对数量大于哈希表大小(载荷因子大于1) 且resize标记为1可进行扩张
// static unsigned int dict_force_resize_ratio = 5;
// 若是 resize标记为0,则载荷因子大于5 可进行扩张
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
// 哈希表长度扩张为 2的次方中第一个大于等于已有键值对数量两倍
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
}
复制代码
当不存在持久化子进程时,载荷因子>=1时扩张,扩张后长度为2的次方中首个>= used(已有键值个数) * 2的数。例如: 本来哈希表长度是5,有10个键值对。扩张后长度是32。2 4 8 16 32...中第一个大于10 * 2的是32。
而存在持久化子进程时载荷因子>=5才能够扩张,这是为了不子进程写时复制致使的没必要要的内存分配。
#define HASHTABLE_MIN_FILL 10 /* Minimal hash table fill 10% */
int htNeedsResize(dict *dict) {
long long size, used;
size = dictSlots(dict);
used = dictSize(dict);
// 负载因子小于 0.1则进行收缩
return (size > DICT_HT_INITIAL_SIZE &&
(used*100/size < HASHTABLE_MIN_FILL));
}
/* 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;
// 只有resize标记为1且当前不处于rehash状态时能够进行resize操做
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used;
// #define DICT_HT_INITIAL_SIZE 4
if (minimal < DICT_HT_INITIAL_SIZE)
minimal = DICT_HT_INITIAL_SIZE;
// 哈希表长度缩小为 2的次方中第一个大于等于 4与当前已拥有键值对数量中的较小值
return dictExpand(d, minimal);
}
复制代码
载荷因子< 0.1时收缩,收缩后哈希表长度为 4与used(已拥有键值对个数)中的较小值,这个动做只有不存在持久化子进程且不处于rehash状态时进行。后者好理解,可是有子进程时为啥扩张的时候只是调高了执行条件,收缩的时候直接就不让执行了?
由于写时复制只要是父进程的内存发生变化,子进程就会进行内存分配。而前面说了,须要扩张是由于查询效率过低了,性能的下降对于redis是不能接受的。而须要收缩时,仅仅只是浪费了一点内存没有释放,短期内是能够接受的。
分析完了rehash中的收缩和扩张的策略,咱们再来看下rehash具体是怎么执行的。 前边咱们说了dict结构有两个哈希表,多出来的那个哈希表就是用来rehash中临时使用的。 具体步骤以下:
int dictExpand(dict *d, unsigned long size) {
// ...
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
复制代码
// 执行一步rehash, 迁移d->ht[0]中rehashidx对应索引以后第一个非空元素(多是一个链表)到备胎上
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1);
}
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++;
}
// ...
/* More to rehash... */
return 1;
}
复制代码
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
// ...
/* 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;
}
复制代码
rehash为何要搞成渐进处理?
当字典数据量小的时候,rehash一次性搞定很快很方便,感受如今的这种处理方法不少余很繁琐,可是若是数据量比较大的时候,几百万甚至几千万条数据时,只是算个hash值就须要庞大的计算量,若是要一次性搞定服务器就没法正常工做了,即使不gg也会对服务性能形成很大的影响。因此redis采用了愚公移山的办法,一点一点的处理。
而在rehash处理过程当中,删改查等操做查找key都是先找d->ht[0],没找到再找备胎d->ht[1]。以查找key为例:
dictEntry *dictFind(dict *d, const void *key) {
dictEntry *he;
uint64_t h, idx, table;
if (d->ht[0].used + d->ht[1].used == 0) return NULL; /* dict is empty */
// 执行了一步rehash
if (dictIsRehashing(d)) _dictRehashStep(d);
// 计算hash值
h = dictHashKey(d, key);
// 两个哈希表进行遍历
for (table = 0; table <= 1; table++) {
// 取余求索引
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
// 没有进行rehash时,只查询d->ht[0]
if (!dictIsRehashing(d)) return NULL;
}
return NULL;
}
复制代码