围着灰机转圈圈

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

我知道点redis-数据结构与对象(字典)

字典在redis中的应用相当广泛,比如redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、改、查操作也是构建在对字典的操作之上的。

使用

  1. redis的数据库
  2. 哈希键
  3. 集合

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;
  • typeprivdata属性是为创建多态字典设置的。
  • 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操作完成,步骤如下:

  1. 为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

  2. 将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的hash值和index值,然后将键值对放置到ht[1]哈希表的指定位置上

  3. 当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的详细步骤

  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个hash表。
  2. 在字典维持一个索引计数器rehashidx,并将它的值设置为0,表示rehash工作正式开始。
  3. 在rehash期间,每次对字典执行增删改查是,程序除了完成指定操作,还会顺带将ht[0]的哈希表在rehashidx索引上的键值对rehash到ht[1],然后将rehashidx属性加一。
  4. 随着字典操作的不断执行, 最终ht[0]上所有的键值对rehash到ht[1]上,这时程序将rehash置为-1,表示rehash完成。

渐进式rehash执行期间的hash表操作

因为在渐进式rehahs期间,字典会同时使用ht[0]和ht[1]。所以在rehash期间,字典的del,find,update操作会在2个hash表上进行。

另外,新添加的键值对回添加到ht[1]上。

posted on 2015-08-05 21:01  围着灰机转圈圈  阅读(199)  评论(0编辑  收藏  举报