一、数据结构 🪜
typedef struct dictEntry {
// key
void *key;
// 这里采用了 union 类型,节省内存
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 下一个节点, 采用链地址法。针对新节点采用头插法。
struct dictEntry *next;
} dictEntry;
// 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;
// 每个 hashtable 都会有两个,将旧的复制到新的
type struct dictht {
dictEntry **table;
unsigned long size; //hash表的大小
unsigned long sizemask; //mask计算索引值
unsigned long used; //表示已经使用的个数
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
// 新的 dictht 和旧的 dictht,一般只会用 0,ht[1]哈希表只会对 ht[0] 哈希表进行 rehash 操作。
dictht ht[2];
//如果 rehashindex == -1 表示不会进行 rehash
long rehashindex;
unsigned long iterators;
} dict;
二、重点解释 🌈
2.1 dictht 扩容
// 对齐 2 的指数。
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;
}
}
2.2 rehashindex
- dict 中的 rehashindex 标记是否在进行 rehash,默认为 -1 表示未进行,n 表示当前 dt[0]中第n 个 tb 实例。
// 等于 -1;
#define dictIsRehashing(d) ((d) -> rehashidx != -1)
//不等于 -1;
if(!dictIsRehashing(d)) return 0;
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
assert(d->ht[0].size > (unsigned long)d -> rehashidex);
// 空桶的话,就直接跳过,如果超过 10*N 个空桶,此时也先切换,不能长时间占用
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
// 表明该桶不为空
de = d->ht[0].table[d->rehashidx];
2.3 哈希算法
在 redis 中 hash 默认使用的是 siphash 算法。计算哈希值和索引值方法如下:
- 使用字典设置的哈希函数,计算键key 的哈希值
hash = dict->type->hashFunction(key);
- 使用哈希表的 sizemark 属性和第一步得到的哈希值,计算索引值
//取模运算 index = hash & dict->ht[x].sizemark;
2.3.1
- 哈希冲突
redis 解决哈希冲突的方法时链地址法,而且采用的是头插法。 - 哈希扩容
采用链地址法来解决冲突,如果数据量较大,大量的冲突会导致某个桶上的链表非常长,不利于数据查询,因此需要根据负载因子来判断当前是否需要扩容,见函数 _dictExpandIfNeeded。其中 resize 还与是否正在进行持久化有关。 - 缩容
redis 中进行 resize 的条件是由两处决定的即 htNeedResize 和 dictResize。- 超过了初始值且填充率小于 10%,这说明需要扩容。
发生的时机主要是在每次数据移除和 serverCron 定时任务中: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)); }
intsetTYpeRemove(robj *setobj,sds value){ long long llval; if(setobj-encoding == OBJ_ENCODING_HT){ if(dictDelete(setobj->ptr.value) == DICT_OK) { if(htNeedsResize(setobj->ptr)) dictResize(setobj->ptr); return 1; } } }
- 超过了初始值且填充率小于 10%,这说明需要扩容。
2.3.2 rehash 优化
- rehash过程,ht[0中会存在空节点,为了防止阻塞,每次限定如果连续访问 10*N(N表示步长)个空节点后,直接返回。见dictRehash函数
- 渐进式hash。是指 rehash 操作不是一次性、集中式完成的。
- 第一类:正常操作见dictRehash函数,但是对于 Redis 而言,如果 Hash 表的 key 太多,这样可能导致 rehash 操作需要长时间进行,阻塞服务器,所以 Redis 本身将 rehash 操作分散在了后续的每次增删改查中(以桶为单位)。
- 第二类: 针对第一类间接式 rehash,存在一个问题:如果 A 服务器长时间处于空闲状态,导致哈希表长期使用 0 和 1 号两个表。为解决这个问题,在 serverCron 定时函数中,每次拿出 1ms 时间来执行 Rehash 操作,每次步长 100,但需要开启 activerehashing。
2.3.3 rehash 注意点
- rehash 过程 dictAdd,只插入 ht[1]中,确保 ht[0]只减不增。
- rehash 过程 dictFind 和 dictDelete,需要涉及到 0 和 1 两个表。
- 字典默认的 hash 算法是 SipHash。
- Redis 在持久化时,服务器执行扩展操作所需要的负载因子并不相同,默认为 5.