Redis底层数据结构之字典

Redis中的字典通过哈希表实现,使用链地址法解决键冲突。字典包含两个哈希表,ht[0]为主表,ht[1]用于rehash。rehash是渐进式的,避免一次性大规模计算影响服务。当负载因子超过阈值时,会扩展或收缩哈希表,保持性能稳定。

字典又称为符号表或者关联数组、或映射(map),是一种用于保存键值对的抽象数据结构。字典中的每一个键 key 都是唯一的,通过 key 可以对值来进行查找或修改。C 语言中没有内置这种数据结构的实现,所以字典依然是 Redis自己构建的。

Redis的字典是用哈希表作为底层实现的,一个哈希表中可以有多个哈希表节点,每个哈希表节点保存字典中的一个键值对。

哈希表结构

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

在这里插入图片描述
哈希表节点

typedef struct dictEntry{
​	void *key;//键
​	union{//值
​		void *val;
​		uint64_t u64;
​		int64_t s64;} v;
​	struct dictEntry *next;//指向下个哈希表节点,形成链表
} dictEntry;

next属性是指向另一个哈希表节点的指针,作用是将多个哈希值相同的键值对连接在一起,形成链表(如下图)。

在这里插入图片描述

3、字典

typedef struct dict{
​	dictType *type;//类型特定函数
​	void *privdata;//私有数据
​	dictht ht[2];//哈希表
​	int rehashidx;//rehash索引,当rehash不在进行时,值为-1
} dict;

type 和privdata属性是针对不同类型的键值对,为创建多态字典而设置的。

ht属性是一个包含两个dicyht哈希表的数组,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。

rehashidx属性记录了rehash目前的进度,如果目前没有进行rehash,那么其值为-1

在这里插入图片描述
4、哈希算法

当要将一个新的键值对添加到字典中时,需要根据键计算出哈希值和索引值,然后根据索引值将包含新键值对的哈希表节点放到哈希表数组的指定索引上。

哈希值:
hash = dict->type->hashFunction(key) //使用字典设置的hashFunction函数计算key的哈希值

索引值:
index = hash & dict->ht[x].sizemask //使用哈希值和sizemask计算索引值,ht[x]根据情况不同可以是ht[0]或ht[1]

5、解决键冲突

类似hashmap使用链地址法解决冲突。

新节点添加到链表的表头位置(头插法)

6、rehash

随着操作的不断执行,键值对会逐渐增多或减少,为了将负载因子维持在合理范围,需要对哈希表大小进行相应的扩展或收缩。

扩展或收缩通过rehash操作来完成,步骤如下:

(1)为ht[1]哈希表分配空间,空间大小取决于ht[0].used属性的值

若是扩展操作,那么ht[1]的大小为第一个>=ht[0].used*2的2^n (如果ht[0].used=4,那么4乘2=8,而8恰好是第一个>=4的2的n次方,那么ht[1]的大小就应该分配为8)
若是收缩操作,那么ht[1]的大小为第一个>=ht[0].used的2^n
(2)将保存在ht[0]中的所有键值对rehash到ht[1]中,rehash指重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。

(3)当ht[0]的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],新建空白的哈希表ht[1],以备下次rehash使用

扩展与收缩的条件
(1)当以下条件满足任意一个时,程序就会对哈希表进行扩展操作
服务器目前没有执行bgsave或bgrewriteaof命令,并且哈希表的负载因子>=1
服务器目前正在执行bgsave或bgrewriteaof命令,并且哈希表的负载因子>=5
(2)当负载因子的值小于0.1时,程序就会对哈希表进行收缩操作
负载因子的计算
load_factor = ht[0].used / ht[0].size

7、渐进式rehash

扩展或收缩哈希表需要将 ht[0] 里面的所有键值对 rehash 到 ht[1] 里面, 但是, 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。

原因在于如果键值对的数量太多的话,要一次性将所有键值对全部rehash到ht[1]中的话,庞大的计算量可能会导致服务器再一段时间内停止服务。

渐进式rehash的步骤

为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
维持索引计数器变量rehashidx,并将它的值设置为0,表示rehash开始
每次对字典执行增删改查时,将ht[0]的rehashidx索引上的所有键值对rehash到ht[1],将rehashidx值+1。
当ht[0]的所有键值对全部被rehash到ht[1]中,程序将rehashidx的值设置为-1,表示rehash操作完成

渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对的计算均摊到每个字典的增删改查操作上,避免了集中式rehash的庞大计算量。

一次完整的渐进式rehash过程如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在此过程中,字典的增删改查操作会同时在ht[0],ht[1]两个表上进行,比如:

查找一个键,会先在ht[0]中查找,没找到再到ht[1]中找。
新添加到字典的键值对一律会被保存到ht[1]中而不是ht[0].

总结
(1)redis字典使用哈希表作为底层实现,哈希表使用链地址法解决键冲突
(2)每个字典有两个哈希表,ht[0]在平常使用,ht[1]在rehash时使用
(3)rehash并不是一次性完成的,而是渐进式的进行。

Redis是一个开源的内存数据库,它使用了多种数据结构来存储不同类型的数据。下面是几种常见的Redis底层数据结构的详解: 1. 字符串(String):字符串是Redis中最基本的数据结构。它可以存储任意类型的数据,包括数字、文本等。字符串在Redis中以字节数组的形式存储,可以通过键访问和修改。 2. 列表(List):列表是一个有序的字符串集合,可以在列表的两端进行插入、删除和获取操作。Redis使用双向链表来实现列表数据结构,它支持快速插入和删除操作。 3. 哈希(Hash):哈希是一种键值对的集合。在Redis中,哈希可以存储多个字段和对应的值,类似于关联数组或者字典。哈希在内部使用哈希表来实现,可以快速找和修改字段值。 4. 集合(Set):集合是一组唯一且无序的字符串集合。Redis使用哈希表来实现集合数据结构,它支持添加、删除和判断元素是否存在等操作。 5. 有序集合(Sorted Set):有序集合是一组唯一且有序的字符串集合。在Redis中,每个元素都会关联一个分数,通过分数可以对元素进行排序。有序集合的实现使用了跳跃表和哈希表两种数据结构,它支持添加、删除、修改和范围询等操作。 这些数据结构底层实现都是高效的,并且支持丰富的操作。Redis数据结构灵活性较高,能够满足不同类型的数据存储需求。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值