我知道点redis-数据结构与对象(字典)
字典在redis中的应用相当广泛,比如redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、改、查操作也是构建在对字典的操作之上的。
使用
- redis的数据库
- 哈希键
- 集合
4.1 字典的实现
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
4.1.1 哈希表
Redis字典所使用的哈希表由dict.h/dictht
结构定义:
typedef struct dictht {
//哈希表数组
//数组中的每个元素都是一个指向`dict.h/dictEntry`结构的指针,每个`dictEntry`保存着一个键值对;
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值,这个属性和哈希值一起决定一个key被放到table的哪个index上。
//总是等于size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht;
4.1.2 哈希表节点
哈希表节点使用dictEntry
结构表示,每隔dictEntry
结构都保存着一个键值对:
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成LinkedList
// 将多个哈希值相同的dictEntry连接在一起,以此来解决键冲突(collision)
struct dictEntry *next;
} dictEntry;
4.1.3 字典
Redis中的字典由dict.h/dict
结构表示:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash索引 , 当reshah不在进行时, 值为-1
int rehashidx;
} dict;
type
和privdata
属性是为创建多态字典设置的。ht
属性是一个包含两个项的数组,数组中的每个项都是一个dictht
哈希表,一个情况下,字典只使用ht[0]
,ht[1]
哈希表只会在对ht[0]
进行rehash
时使用。- 除了
ht[1]
之外,另一个和rehash
有关的属性就是rehashidx
,他记录了rehash
目前的进度,如果没有进行rehash
,它的值是-1
。
4.2 哈希算法
当要将一个新的键值对添加到字典里,程序先根据键的值计算出哈希值和索引值,然后根据索引值,将dictEntry
添加到哈希表数组指定索引上面。
#使用字典设置的hash函数,计算key的哈希值
hash = dict->type->hashFunction(key);
#使用hash表的sizemask属性和hash值,计算index。根据情况不同,ht[x]可以是ht[0]或者ht[1]
index = hash & dict->ht[x].sizemask;
Examples:
rehashidx = -1; sizemask = 3;
hash = dict->type->hashFunction(k0);
假设 hash = 8
index = hash & dict->ht[0].sizemask = 8 & 3 = 0;
当字典被用作数据库的底层实现,或者hash键的底层实现时,Redis使用MurmurHash2算法来计算键的hash值。这个算法的有点在于,即使输入有有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。
4.3 解决键冲突
redis的哈希表使用链地址法来解决键冲突,每个hash表节点都有一个next
指针,多个hash表节点可以用next指针构成一个单向链表。
4.4 reshah
随着操作的不断执行,哈希表保存的键值对会主键地增多或者减少,为了让哈希表的负载因子(load factor) 维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少,程序需要对哈希表的大小相应的扩展或者收缩。
扩展或者收缩hash表的工作可以通过执行rehash操作完成,步骤如下:
-
为ht[1]分配空间,这个hash表的空间大小取决于要执行的操作,以及ht[0]当前包括的键值对数量(ht[0].used):
- 扩展操作:ht[1]的大小为第一个
>=ht[0].used*2
的2^n; - 收缩操作:ht[1]的大小为第一个
>=ht[0].used
的2^n;
扩展操作,ht[0].used=4,4*2=8,而8恰好是第一个>=4的2的n次方,所以ht[1].size=8
- 扩展操作:ht[1]的大小为第一个
-
将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的hash值和index值,然后将键值对放置到ht[1]哈希表的指定位置上
-
当ht[0]包含的所有键值对都迁移到ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白hash表,为下一次rehash做准备。
哈希表扩展条件
- 服务器目前没有执行BGSAVE或者BGREWRITEAOF命令,并且哈希表的负载因子
>=1
- 服务器目前正在执行BGSAVE或者BGREWRITEAOF命令,并且hash表的负载因子
>=5
在执行BGSAVE或者BGREWRITEAOF的过程中,redis创建当前服务器进程的子进程,大多数OS都会采用写时复制技术来优化子进程的使用效率,所以在子进程存在期间,服务器回提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表的扩展操作,这样可以避免不必要的内存写入。
哈希表收缩条件
哈希表的负载因子小于0.1
4.5 渐进式rehash
扩展或者收缩hash表需要将ht[0]里面所有键值对resh到ht[1]里面,但是这个rehash动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。这样做的原因是如果ht[0]中存在很多键值对,要一次性rehash,那么会导致服务器在一段时间内停止服务。
渐进式rehash的详细步骤
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个hash表。
- 在字典维持一个索引计数器rehashidx,并将它的值设置为0,表示rehash工作正式开始。
- 在rehash期间,每次对字典执行增删改查是,程序除了完成指定操作,还会顺带将ht[0]的哈希表在rehashidx索引上的键值对rehash到ht[1],然后将rehashidx属性加一。
- 随着字典操作的不断执行, 最终ht[0]上所有的键值对rehash到ht[1]上,这时程序将rehash置为-1,表示rehash完成。
渐进式rehash执行期间的hash表操作
因为在渐进式rehahs期间,字典会同时使用ht[0]和ht[1]。所以在rehash期间,字典的del,find,update操作会在2个hash表上进行。
另外,新添加的键值对回添加到ht[1]上。