Redis数据结构之字典--Redis设计与实现的读书笔记


参考书籍:《Redis设计与实现》
Redis使用的C语言并没有实现字典这一数据结构,因此Redis构建了自己的字典的实现。字典在Redis的应用相当广泛,Redis数据库就是利用字典来作为底层实现的,对数据库的增删查改都是构建在字典的操作之上,字典还是哈希键的底层实现之一。

1 字典的实现

Redis底层是用哈希表来实现的,一个哈希表可以有多个哈希节点,一个节点包含了字典中的一个键值对。

1.1 哈希表

Redis字典使用的哈希表由dict.h/dictht结构定义:

typedef struct dictht {
	dictEntry **table;
	unsigned long size;
	unsigned long sizemask;
	unsigned long used;
} dictht;

table属性是一个数组,数组的每一个元素都是指向dict.h/dictEntry结构的指针,dictEntry结构在1.2会提到。size属性记录了哈希表的大小,used属性记录目前已有节点的数量,sizemask属性总是等于use-1,作用是用于计算索引值。
在这里插入图片描述

1.2 哈希节点

哈希节点使用dictEntry结构表示:

typedef struct dictEntry {
	void *key;
	union {
		void *val;
		uint64_tu64;
		int64_ts64;
		} v;
	struct dictEntry *next;
}

key属性包含了键值对中的键,v属性包含了键值对中的值,值可以是指针,也可以是uint64_t整数,或者是int64_t整数,next属性是指向下一个哈希节点的指针,这个指针的作用是把多个哈希值相同的键值对连接在一起,以此解决键冲突的问题。
在这里插入图片描述

1.3 字典

Redis的字典以dict.h/dict结构表示:

typeder struct dict {
	dictType *type;
	void *privdata;
	dictht ht[2];
	int rehashidx;
} dict;

type属性是指向dictType结构的指针,每个dictType结构保存了一组用于操作特性类型键值对的函数,Redis会为类型不同字典设置不同的类型特定函数;
privdata属性保存了需要传给类型特定函数的可选参数;
dictType结构如下:

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 *keyl, const void *key2); 
	void (*keyDestructor) (void *privdata, void *key); 
	void (*valDestructor) (void *privdata, void *obj);
} dictType;

ht是包含两项的数组,每一项都是一个dictht哈希表,一般情况下只使用ht[0]哈希表,ht[1]会在对ht[0]进行rehash时进行调用。除了ht[1]之外,另一个和rehash有关的属性是rehashidx,它记录了rehash当前的进度,如果没有在进行rehash,那么它的值为-1。
普通状态下的字典如下所示:
在这里插入图片描述

2 哈希算法

当需要把一个新的键值对添加到字典里面时,程序会先根据键值对的键计算出哈希值和索引值,再根据索引值,把包含键值对的哈希表节点放到哈希表数组的指定索引上。
Redis中计算哈希值和索引值的方法如下:

hash = dict->type->hashFunction(key);
index = hash & dict->ht[x].sizemask;

例如,要把键值对k0,v0放到字典中,首先把key进行hashFunction计算,加入得到8,再计算8&3=0,表示k0,v0键值对应该放到索引为0的位置上。
在这里插入图片描述
当字典被用作数据库的底层实现时,或者哈希表的底层实现时,Redis使用MurmurHash2算法来计算哈希值。MurmurHash算法最初由Austin Appleby于20O8年发明,这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。MurmurHash算法目前的最新版本为MurmurHash3,而Redis使用的是MurmurHash2,关于MurmurHash算法的更多信息可以参考该算法的主页:链接: link

3 解决键冲突

当有两个或以上的哈希节点被分配到哈希表数组同一个索引时,称这些键发生了冲突。Redis使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点通过next指针构成一个单向链表,解决了键冲突的问题。
在这里插入图片描述

4 rehash

随着操作的不断执行,哈希表数组保存的键值对会逐渐增多或减小,为了让哈希表的负载因子维持在一个合理的范围内,程序会对哈希表大小进行拓展或收缩。拓展和收缩可以通过rehash(重新散列)来完成,步骤如下:

  1. 为字典的ht[1]哈希表分配空间,这个空间大小取决于要执行的操作,以及ht[0]包含的键值对数量(ht[0].used的大小)
    1.1 如果执行的是拓展操作,那么ht[1]大小为大于等于ht[0].used*2的第一个2n
    1.2 如果执行的是收缩操作,那么h[1]大小为大于等于ht[0].used的第一个2n
  2. 将保存到ht[0]的所有键值对rehash到ht[1]上面,rehash指的是重新计算hash值和索引值,再放到ht[1]的指定索引上。
  3. 在ht[0]都rehash到ht[1]之后,将ht[0]空间释放,ht[1]置为ht[0],ht[0]置为ht[1],为新ht[1]创建空白哈希表,为下一次rehash做准备。

4.1 哈希表的拓展与收缩

当下面两个条件中的一个被满足是,程序对哈希表执行拓展操作:

  1. 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1
  2. 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5

其中哈希表负载因子计算公式为:
load_factor = ht[0].used/ht[0].size;
区分两种情况是因为Redis在BGSAVE和BGREWRITEAOF时,需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制的技术来优化子进程使用效率,所以需要尽量避免在子进程存在期间进行哈希表拓展,可以避免不必要的内存写入操作,最大限度节约内存。
另一方面,当负载因子小于0.1时,程序对哈希表进行收缩操作。

4.2 渐进式rehash

渐进式rehash是说将键值对从ht[0]移到ht[1]的过程是分多次,渐进式地完成。这样做的原因是,如果ht[0]里面包含的键值对数量非常大,比如百万,千万甚至上亿时,全部一次性rehash会让服务器在一段时间内停止服务。因此,为了避免这种情况,服务器是分多次,渐进式地把ht[0]中的键值对rehash到ht[1]中的,步骤如下:

  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
  2. 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始
  3. 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一
  4. 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成

渐进式rehash的好处是采取了分而治之的方式,将rehash的操作均摊到对字典的每个增加、删除、查询、修改操作上,从而避免了集中式rehash所带来的巨大计算量。而且增加操作只会在ht[1]中进行,保证了每次都会使ht[0]中的键值对数量减一,直到成为空表。

5 字典API

函数作用时间复杂度
dictCreate创建一个新的字典O(1)
dictAdd将给定的键值对添加到字典里面O(1)
dictRepalce将给定的键值对添加到字典里面,如果键已经存在于字典,那么用新值取代原有的值O(1)
dictFetchValue返回给定键的值O(1)
dictGetRandomKey从字典中随机返回一个键值对O(1)
dictDelete从字典中删除给定键所对应的键值对O(1)
dictRelease释放给定字典,以及字典中包含的所有键值对O(N),N为字典包含的键值对数量
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值