Redis-字典数据结构-源码深入解析

10 篇文章 0 订阅

1.Redis的字典是什么?

Redis字典,又叫符号表/关联数组/映射,是一种用于保存键值对的抽象数据结构。还可以作为Redis数据库的底层,哈希键的底层等。

2.如何实现Redis字典

自顶向下来实现一个Redis字典,使用Redis的实现语言-C语言

1)字典

typedef struct dict{
	dicType *type;//类型特定函数
	void *privdata;//私有数据
	dictht ht[2];//哈希表
	int trehashidx;//rehash索引,当rehash不再进行时,值为-1
}
  • type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:
    – privdata属性:保存了需要传给那些特定类型函数(见下)的可选参数
    – type属性:指向一个dictType结构的指针,每个该结构都保存了一簇用于操作特定类型的键值对的函数,Redis自动为不同的字典类型设置类型特定函数`
typedef struct dictType{
	unsigned int (*hashFunction)(const void *key);//计算哈希值的函数
	void *(*keyDup)(void*privdata,const void *key);//复制键的函数
	void *(*valueDup)(void*privdata,const void *obj);//复制值的函数
	int (*keyCompare)(void *privadata,const void *key1,const void *key2);//对比值的函数
	int (*keyDestructor)(void *privdata,void *key);//销毁键的函数
	int (*valDestructor)(void *privdata,void *obj);//销毁值的函数

  • ht属性是包含了两个项的数组,每个项都是一个dictht哈希表(详情见下)。一般情况,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]进行rehash的时候使用。
  • rehashidx属性也是和rehash有关的,记录了当前rehash的进度,默认是-1。
    普通状态下的字典
    上图是普通状态下的字典,箭头表示将表中的小项进行展开,本节也是按照这种结构撰写的

2)哈希表

Redis字典的底层数据结构包括哈希表结构

typedef struct dictht{
	dictEntry **table;//哈希表数组
	unsigned long size;//哈希表大小
	unsigned long sizemask;//哈希表大小掩码,用于计算索引值。sizemask = size - 1;
	unsigned long used;//该哈希表已有的节点的数量
}

哈希表数组table的每个元素指向一个dictEntry结构,也就是哈希表节点

3)哈希表节点

字典以哈希表作为底层实现,一个哈希表包括多个哈希节点,一个哈希节点保存一个字典中的键值对。

typedef struct dictEntry{
	void *key;//键
	/*值*/
	union{//三种类型的数据
		void *val;
		unit64_t u64;
		int64_t s64;
	}v;//值的名字,有以上三种选择
	struct dictEntry *next;//指向下一个哈希节点,形成链表,解决哈希冲突问题,关于哈希冲突将在下边进行介绍
}

3.哈希算法

1)什么是哈希算法

  • 在学习HashMap的时候我们对hash算法有了一定的了解.。 在Redis中,当一个新的键值添加到字典中时,程序会根据键值对的键计算出哈希值和索引值,根据索引值将包含新键值对的哈希表节点放入 哈希表数组 的指定索引上。
  • 键的哈希值使用哈希函数hash=dict->type->hashFunction(key)计算,之后通过哈希表的sizemask属性值和哈希值,使用公式index=hash&dict->ht[x].sizemask计算出索引值,其中ht[x]中的x可能是0或1。
  • 关于MurmurHash算法可以看这个网站:算法主页

2)如何解决哈希冲突

  • 哈希冲突:当两个及以上的键被分配到哈希表数组的同一个索引上面时,这些键发生哈希冲突。
  • Redis哈希表使用链地址法解决键冲突:前边讲哈希表节点的结构时讲过,哈希表节点有一个next指针指向下一个哈希节点,多个哈希表节点可以用指针构成一个单项链表,针对分配到同一个索引上的多个节点可以奖它们用链表连接,这样就有效解决了哈希冲突问题。
  • 一些使用中的惯例:新添加的节点要添加到链表的表头位置,因为dictEntry节点没有指向表尾的指针。

3)rehash

  • 背景:随着不断的操作,哈希表保存的键值对也在增加/减少,为了维持哈希表的负载因子在合理的范围内,需要对哈希表的大小进行管理:键值对数量过多就扩展,数量过少就收缩。
  • 以上工作都是通过rehash(重新散列)操作完成,rehash的步骤如下:
    – 1、为字典的ht[1]分配空间:取决于要执行和操作、ht[0]当前包含键值对的数量。
    如果要扩展哈希表,ht[1]的大小是2的n次方,且恰好大于等于ht[0].used*2的那个
    如果是收缩哈希表,ht[1]的大小是2的n次方,且恰好大于等于ht[0].used的那个
    – 2、将ht[0]上的所有键值对rehash到ht[1]上,rehash就是重新计算键的哈希值和索引值,之后将键值对放在新的哈希表的指定位置上
    – 3、ht[0]的所有键值对迁移到ht[1]之后,释放空表ht[0],将ht[1]设置为ht[0],设置一个空白的哈希表为新的ht[1](为下一次rehash做准备)
  • 什么时候会发生哈希表的扩展?
    – 1、服务器当前没有执行BGSAVE命令/BGREWRITEAOF命令,并且哈希表的负载因子>=1
    – 2、服务器当前正在执行BGSAVE命令/BGREWRITEAOF命令,并且哈希表的负载因子>=5
    负载因子的计算公式如下:
//负载因子 = 哈希表已保存的节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size;
  • 什么时候会发生哈希表的收缩?:负载因子 < 0.1时。

4) 渐进式rehash

  • 什么时候使用渐进式rehash?
    当ht[0]的键值对数量不多的时候,可以一次性rehash到ht[1]中。当ht[0]保存的键值对数量到了百万级别这种很大的规模的时候,一次性的rehash到ht[1]中计算量太大可能导致计算机崩溃,这时需要分多次、渐进的将ht[0]的键值对rehash到ht[1]中。
  • 怎么执行渐进式rehash操作?
    1、先根据ht[0]的键值对数量判断是否要执行渐进式rehash,之后为ht[1]分配空间、
    2、在字典中维持一个索引计数器变量rehashidx = 0,表示rehash工作开始
    3、在rehash期间,对字典执行增删改查操作,还会将ht[0]在rehashidx索引上的所有键值对rehash到ht[1]上,rehash完成时rehashidx++
    4、当所有的键值对都被rehash到ht[1]上是,rehashidx = -1表示rehash操作结束
    分治思想在rehash中得到了使用,将操作均摊到每个添加、删除、查找、更新操作上,避免集中式rehash带来的巨大计算量

3.Redis关于字典的API

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值