Redis数据结构之Dict字典
Posted BBinChina
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis数据结构之Dict字典相关的知识,希望对你有一定的参考价值。
简介
字典(Dict),是一种用于保存键值对(key-value pair)的抽象数据结构,其应用于将一个键(key)映射为一个值,从而形成关联性。
在字典中,每个键都是独一无二的,通过独一无二的特性来获取、更新、删除与之关联的值。
键的独一无二特性取决于哈希函数的随机性,当我们实现自己的字典时,哈希函数是实现的一大关键。
字典这种结构通常内置在各个高级语言,比如c++的Map、java的Map,都是字典。
字典在redis中的应用场景
Redis的实现语言为C,因此Redis的作者自己实现了字典这个数据结构。
redis数据库
Redis的数据库采用字典作为底层实现,其增、删、改、改操作都是通过字典进行操作的。
typedef struct redisDb {
// 数据库键空间,保存着数据库中的所有键值对
dict *dict; /* The keyspace for this DB */
// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
dict *expires; /* Timeout of keys with a timeout set */
// 正处于阻塞状态的键
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */
// 可以解除阻塞的键
dict *ready_keys; /* Blocked keys that received a PUSH */
// 正在被 WATCH 命令监视的键
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */
// 数据库号码
int id; /* Database ID */
// 数据库的键的平均 TTL ,统计信息
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;
redis作为nosql,其数据库结构 redisDb 采用dict来存储数据库中的所有键值对。
通过源码搜索下dict类型,便可以知道redis在哪些部分使用到了dict,redis的源码地址:
github
字典的实现
接下来讲解redis里的字典实现,在后面的内容介绍 c++ 以及 java的内置数据结构
redis的字典采用哈希表,哈希表由多个节点组成,每个节点保存字典中的一个键值对。
字典实现对应源码文件为:dict.h、dict.c
字典结构图,对应的源码:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
字典由两个哈希表组成,ht[0] 表示当前存储的键值对,而可以将ht[1]视为用于rehash时进行扩展或收缩的临时数据,介绍rehash时再介绍其用法。
哈希表 dictht
哈希表结构源码如下:
/*
* 哈希表
*
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
// 哈希表节点数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
1、table是一个数组,数组的每个元素为dictEntry结构的指针,每个dictEntry即保存着一个键值对。
2、size是数组的大小,采用无符号long类型
3、used是已存有的键值对数量
4、sizemask用于计算键值对的索引,因为数组下标从0开始,所以其值等于size-1
哈希表节点dictEntry
节点结构的源码如下:
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
1、键值对的值可以是指针,指向redis内的其他结构对象,也可以是一个uint64_t、int64_t整数
2、next指针用于指向下一个键值对节点,是解决哈希冲突的最常见方式:链地址法。当k1的哈希值与k0哈希值一致时,其保存的哈希节点在相同的表索引位置。因为dictEntry节点组成的链表没有指向链表表尾的指针,所以为了速度考虑,总是将新节点添加在链表的表头:假设[0]节点为k0,现在新增k1
k1->next = dictEntry[0];
dictEntry[0] = k1
字典类型特定函数dictType和私有数据privdata
1、type属性为dictType结构,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数,用于针对不同类型的键值对实现多态
2、private属性保存了需要传给那些类型特定函数的可选参数
其源码如下:
typedef struct dictType {
// 计算哈希值的函数
unsigned int (*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;
当redis创建字典时,可以设置函数指针到dictType结构中
哈希算法
redis计算哈希值和索引值的方法:
hash = dict->type->hashFunction(key);
index = hash & dict->ht[0].sizemask;
hash算法用于根据键值对计算键值对节点的位置,redis采用的是MurmurHash2算法。
rehash
哈希表的键值对会增加或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围,哈希表需要进行扩展或者收缩。
负载因子:哈希表已保存节点数量/哈希表大小
load_factor = ht[0].used / ht[0].size
1、先为ht[1]分配空间:如果是扩展操作,ht[1]的大小等于ht[0].used * 2 的 2^n 、 如果是收缩操作,h1[1]的大小等于ht[0].used 的 2^n
2、将ht[0]的键值对rehash(重新计算节点位置)放到ht[1]
3、ht[0]在迁移玩所有数据后为空表,即可释放,同时将ht[1]设置成ht[0],并在ht[1]新建一个空白哈希表。
扩展操作:
1、服务器没有执行BGSAVE或者BGREWRITEAOF命令,且负载因子大于等于1
2、正在执行BGSAVE或者BGREWRITEAOF命令,且负载因子大于等于5
负载因子不同时因为执行BGSAVE或者BGREWRITEAOF时,需要创建子进程,提高负载因子可以避免在子进程存在时进行扩展,优化内存使用情况
收缩操作:
负载因子小于0.1
渐进式rehash
rehash执行并不是一次性完成的,为了提高性能,采用渐进式的方式多次rehash。
1、在字典中维持一个索引计数器变量rehashidx,其值为0时表示正在执行rehash。
2、rehash期间,对字典操作时,程序除了执行指定命令(键值对的新增是在ht[1]表上操作),会顺带将ht[0]表在rehashidx索引上的所有键值对rehash到ht[1]上,执行成功后rehashidx值加1
4、ht[0]的所有键值对操作完成后,rehashidx置为-1
5、在rehash期间,键值对的查找会现在ht[0]表上查找,再到ht[1]查找。
以上是关于Redis数据结构之Dict字典的主要内容,如果未能解决你的问题,请参考以下文章