Redis源码之数据类型解析-DICT
当前分析Redis版本为6.2,需要注意。
DICT
,字典数据结构,本质是哈希表。相关文件主要有 dict.h
和 dict.c
。
基础结构
dictEntry
是哈希节点,键值对,其中值采用了联合体 v
,从结构体中的 *next
可以看出,这是个单向链表,必然头节点是单独保存的,挂在哈希表上。
typedef struct dictEntry {
void *key; // 键
union {
void *val;
uint64_t u64; // unsigned
int64_t s64; // signed
double d;
} v; // 值
struct dictEntry *next; // 下一个节点地址
} dictEntry;
接着就是 dictType
字典类型,对字典进行了区别,不同类型的字典相关处理函数可以不同。
typedef struct dictType {
// 哈希函数,根据 Key 生成对应的哈希值
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); // 销毁值函数
int (*expandAllowed)(size_t moreMem, double usedRatio); // 扩容函数?
} dictType;
dictht
哈希表结构体。
typedef struct dictht {
dictEntry **table; // 二级指针,存放哈希节点链表的头指针,主要是为了解决哈希冲突
unsigned long size; // 哈希表大小
unsigned long sizemask; // 哈希表大小掩码 size-1。计算索引 idx = hash & sizemask
unsigned long used; // 已有哈希节点数量
} dictht; // 哈希表
dict
字典。
typedef struct dict {
dictType *type; // 字典类型
void *privdata; // 私有数据
// 哈希表数组,只有两个哈希表,一般情况,只使用第一个哈希表。
// ht[1]只会在ht[0]的rehash时使用
dictht ht[2];
// rehash-idx标识 没有rehash时为-1 rehash 过程索引
long rehashidx;
// rehash 暂停标识
// > 0 表示暂停 < 0 表示编码错误
int16_t pauserehash;
} dict;
dictIterator
字典迭代器。
typedef struct dictIterator {
dict *d; // 当前迭代字典
long index; // 迭代器当前所指向的哈希表索引位置
// table 当前被迭代的哈希表 0|1
// safe 标识当前迭代器是否安全
// safe为1是安全的,在迭代过程中,可以执行 dictAdd、dictFind和其他函数,对字典进行修改
// safe不为1,只能调用dictNext对字典进行迭代。限制了修改
int table, safe;
// entry 当前迭代节点指针
// nextEntry 当前迭代节点的下一个节点,在安全迭代器运行时,entry节点可能会被修改
// 需要额外指针保存下一节点,防止指针丢失
dictEntry *entry, *nextEntry;
// 用于误用检测的不安全迭代器签名
long long fingerprint;
} dictIterator;
宏定义函数
DICT_NOTUSED
未使用的参数会生成警告?
dictFreeVal
释放指定字典的指定哈希节点值,在定义了值析构函数的情况下,如果没有定义的话,将不会处理。
#define dictFreeVal(d, entry) \
if ((d)->type->valDestructor) \
(d)->type->valDestructor((d)->privdata, (entry)->v.val)
dictSetVal
设置指定字典的指点哈希节点值,采用 do/while
结构,保证代码执行,一次。在定义了 valDup
的情况下,使用返回值。反之,直接设置。
#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)
dictSetSignedIntegerVal
设置哈希节点值,有符号整型。直接对 entry->v.s64
进行赋值。
dictSetUnsignedIntegerVal
设置哈希节点值,无符号整型。直接对 entry->v.u64
进行赋值。
dictSetDoubleVal
设置哈希节点值,双精度型。直接对 entry->v.d
进行赋值。
dictFreeKey
释放指定字典的指定哈希节点键,同 dictFreeVal
相似。
dictSetKey
设置指定字典的指定哈希节点键,同 dictSetVal
相似。
dictCompareKeys
比较指定字典的两个哈希键。如果存在 d->type->keyCompare
,就调用 keyCompare
进行比较,否则直接 key1 == key2
比较。
dictHashKey
生成指定字典指定键的哈希值。
dictGetKey
获取指定哈希节点键。
dictGetVal
获取指定哈希节点值。
dictGetSignedIntegerVal
获取指定哈希节点的有符号整型值。
dictGetUnsignedIntegerVal
获取指定哈希节点的无符号整型值。
dictGetDoubleVal
获取指定哈希节点的双精度值。
dictSlots
获取指定字典的插槽总量 ht[0].size + ht[1].size
。
dictSize
获取指定字典的哈希表已使用节点总量。
dictIsRehashing
判断指定字典是否正在 rehash。
dictPauseRehashing
暂停指定字典 rehash 过程。
dictResumeRehashing
继续指定字典 rehash 过程。
常量和变量
DICT_OK
宏定义常量。表示字典处理成功。
DICT_ERR
宏定义常量。表示字典处理失败。
DICT_HT_INITIAL_SIZE
宏定义常量。哈希表初始化大小。
dictTypeHeapStringCopyKey
外部变量。字典类型,堆字符串拷贝键?
dictTypeHeapStrings
外部变量。字典类型,堆字符串?
dictTypeHeapStringCopyKeyValue
外部变量。字典类型,堆字符串拷贝键值?
dict_can_resize
静态变量。哈希表大小是否可调整。但该值为0,也不是所有调整都被阻止。
dict_force_resize_ratio
静态变量。哈希表大小强制调整比例。 used/size
,如果该比例大于 dict_force_resize_ratio
,就需要强制调整。
dict_hash_function_seed[16]
静态变量。哈希函数种子。
哈希函数
dictSetHashFunctionSeed
设置哈希种子。使用 memcpy
实现。
dictGetHashFunctionSeed
获取哈希种子。直接返回 dict_hash_function_seed
即可。
dictGenHashFunction
哈希函数。套壳 siphash
实现,具体实现在 siphash.c
文件。
dictGenCaseHashFunction
也是哈希函数。套壳 siphash_nocase
实现,具体实现在 siphash.c
文件。
私有函数
_dictExpandIfNeeded
如果需要,扩展哈希表。调用 dictExpand
接口函数实现。
static int _dictExpandIfNeeded(dict *d)
{
// 正在 rehash 过程,不支持扩展,直接返回即可
if (dictIsRehashing(d)) return DICT_OK;
// 如果空哈希表,直接扩展到初始大小 DICT_HT_INITIAL_SIZE
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
// 条件一 哈希表已使用大小 >= 哈希表大小,说明已经饱和或溢出
// 条件二 dict_can_resize 是否支持大小调整 或 已达到强制扩展比例(负载因子)
// 条件三 该字典类型允许扩展调整
// 扩展到 used+1 大小。。。这样该函数触发可能比较频繁。只增已有节点+1。
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;
}
_dictNextPower
将初始哈希表大小做幂级扩展,直到大于等于给定 size
。
static unsigned long _dictNextPower(unsigned long size)
{
unsigned long i = DICT_HT_INITIAL_SIZE; // 初始大小 4
if (size >= LONG_MAX) return LONG_MAX + 1LU; // 不会溢出?
while(1) {
if (i >= size) // i = 2^n
return i;
i *= 2;
}
}
dictTypeExpandAllowed
判断指定字典是否允许哈希表扩展。主要根据字典类型中的 expandAllowed
函数。
static int dictTypeExpandAllowed(dict *d) {
if (d->type->expandAllowed == NULL) return 1;// 没设置,默认可以?
return d->type->expandAllowed(
_dictNextPower(d->ht[0].used + 1) * sizeof(dictEntry*),
(double)d->ht[0].used / d->ht[0].size);
}
_dictKeyIndex
返回可用插槽的索引(头节点)。如果在 rehash 过程,总是返回 ht[2] 中的索引。
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
// hash 由 key 生成。那这里为什么不调用哈希函数生成,反而传参?
unsigned long idx, table;
dictEntry *he; // 哈希节点
if (existing) *existing = NULL; // 已存在哈希节点。强行重置。
if (_dictExpandIfNeeded(d) == DICT_ERR) return -1; // 扩展哈希表如果有需要
for (table = 0; table <= 1; table++) {
// 在ht[0]和ht[1]中检索,可能在rehash过程。需要循环
idx = hash & d->ht[table].sizemask; // 索引 哈希值和哈希表大小掩码
he = d->ht[table].table[idx]; // 获取相应哈希头节点,开启循环检索 key 毕竟单向链表
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key)) {
// 存在 返回 -1
if (existing) *existing = he;
return -1;
}
he = he->next;
}
if (!dictIsRehashing