字典是一种用来保存键值对的抽象数据结构,在字典中,一个键和一个值进行关联,这些关联的键和值称为键值对。
应用
存储哈希键(值是哈希结构的键,值的底层实现数据结构之一是使用字典)
哨兵模式,管理Master和Slave节点
字典的实现
redis的字典使用哈希表作为底层实现,一个哈希表包含了多个哈希表节点,每个节点就保存了字典中的一个键值对。
Redis中的key-value对,key的数据类型可以是字符串、整数、浮点数(整数和浮点数在底层当做字符串处理),value的数据类型可以是String、Hash、List、Set、SortedSet。
1. 哈希表dictht
哈希表结构定义:
【src/dict.h Redis5.0】
/*
* 哈希表
*
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已存储的数据个数(链表中的数据,而不是占用的slot个数)
unsigned long used;
} dictht;
sizemask为什么等于size-1?
因为Redis中根据key得到的哈希值非常大,不能直接用来当做索引值,因此用哈希值和数组容量做取余运算,就可以得到一个size范围内的值,这个值刚好可以当做索引。而size-1得到的值和哈希值做位与运算,得到的结果和哈希值和size做取余运算一样,并且位运算比取余运算快很多。这就是sizemask值的由来。Redis计算索引值的源码:idx=hash&ht[table].sizemask
一个空的哈希表:
2. 哈希表节点dictEntry
每个节点值都保存着一个
哈希表节点结构定义:
【src/dict.h Redis5.0】
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
union {
void *val; //值,void*类型,保证了value可以是任意类型
uint64_t u64;
int64_t s64; //过期时间
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
含有两个节点的哈希表:
3. 字典dict
字典结构定义:
【src/dict.h Redis5.0】
/*
* 字典
*/
typedef struct dict {
// 该字段对应的特定操作函数(如键的销毁函数、值的销毁函数)
dictType *type;
// 该字典依赖的数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 标识,默认为-1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的迭代器数
int iterators; /* number of iterators currently running */
} dict;
含有两个实体节点的字典:
哈希算法
将新的键值对添加到字典里,需要先根据键值对的键计算出哈希值和索引值,再根据索引值,将包含新键值对的哈希表节点放到哈希表数组指定的索引上。
此外,redis使用链地址法解决哈希冲突,如下图所示,k1和k2冲突后,通过头插法组成链表:
字典扩容-渐进式rehash
- 申请内存的大小为当前Hash表容量的一倍
- 把新申请的内存地址赋值给ht[1],并将rehashidx值设置为0表示开始重新计算哈希值(称为rehash)
- 在rehash期间,对字典执行添加、删除、查找或者操作时,程序除了执行这些操作外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]上,然后rehashidx加1。在服务空闲期间,如果判断当前要进行rehash,也会对哈希表进行rehash。
- 随着字典执行增删改查操作,ht[0]中的所有键值对最终会被rehash到ht[1]上,这时将rehashidx设置为-1,表示rehash完成
- 对调ht[1]和ht[0]的值
部分源码
初始化字典
//dict.h
/* Create a new hash table */
/*
* 初始化字典
*/
dict *dictCreate(dictType *type, void *privDataPtr)
{
dict *d = zmalloc(sizeof(*d));
_dictInit(d,type,privDataPtr);
return d;
}
/* Initialize the hash table */
/*
* 初始化结构体
*/
int _dictInit(dict *d, dictType *type, void *privDataPtr)
{
// 初始化两个哈希表的各项属性值,但暂时还不分配内存给哈希表数组
_dictReset(&d->ht[0]);
_dictReset(&d->ht[1]);
// 设置类型特定函数
d->type = type;
// 设置私有数据
d->privdata = privDataPtr;
// 设置哈希表 rehash 状态
d->rehashidx = -1;
// 设置字典的安全迭代器数量
d->iterators = 0;
return DICT_OK;
}
插入数据
//dict.h
/* Add an element to the target hash table */
/*
* 将给定键值对添加到字典中
* 只有给定键 key 不存在于字典时,添加操作才会成功
*
* 添加成功返回 DICT_OK ,失败返回 DICT_ERR
*
* 最坏 T = O(N) ,均摊 O(1)
*/
int dictAdd(dict *d, void *key, void *val)
{
// 尝试添加键到字典,并返回包含了这个键的新哈希节点
// T = O(N)
dictEntry *entry = dictAddRaw(d,key);
// 键已存在,添加失败
if (!entry) return DICT_ERR;
// 键不存在,设置节点的值
// T = O(1)
dictSetVal(d, entry, val);
// 添加成功
return DICT_OK;
}
/*
* 尝试将键插入到字典中
*
* 如果键已经在字典存在,那么返回 NULL
*
* 如果键不存在,那么程序创建新的哈希节点,
* 将节点和键关联,并插入到字典,然后返回节点本身。
*
* T = O(N)
*/
dictEntry *dictAddRaw(dict *d, void *key)
{
int index;
dictEntry *entry;
dictht *ht;
// 如果条件允许的话,进行单步 rehash
// T = O(1)
if (dictIsRehashing(d)) _dictRehashStep(d);
/* Get the index of the new element, or -1 if
* the element already exists. */
// 计算键在哈希表中的索引值
// 如果值为 -1 ,那么表示键已经存在
// T = O(N)
if ((index = _dictKeyIndex(d, key)) == -1)
return NULL;
// T = O(1)
/* Allocate the memory and store the new entry */
// 如果字典正在 rehash ,那么将新键添加到 1 号哈希表
// 否则,将新键添加到 0 号哈希表
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
// 为新节点分配空间
entry = zmalloc(sizeof(*entry));
// 将新节点插入到链表表头
entry->next = ht->table[index];
ht->table[index] = entry;
// 更新哈希表已使用节点数量
ht->used++;
/* Set the hash entry fields. */
// 设置新节点的键
// T = O(1)
dictSetKey(d, entry, key);
return entry;
}