Redis的链表与字典结构

链表
Redis构建了自己的链表结构,用list进行管理。

typedef struct listNode{
	//前置节点
	struct listNode *prev;
	//后置节点
	struct listNode *next;
	void *value;
}listNode;

typedef struct list {
	listNode *head;
	listNode *tail;
	unsigned long len;
	void *(*dup)(void *ptr);
	void (*free)(void *ptr);
	int (*match)(void *ptr,void *key); 
}list;

可以很清楚的看见,每一个节点就是一个listNode,并且有一个前置指针后后置指针,因此Redis是创建了一个双端队列。我们也可以有listNode来创建链表,但Redis提供了方便管理的list结构。list中有一个头节点和一个尾节点,利用管理与访问。并设置了len属性,是在调用list的api时自动完成赋值,因此访问链表的长度可以直接通过len得出,时间复杂度O(1); 还提供了一些辅助函数:
·dup进行节点的赋值;
·free进行节点的释放;
·match用于比较节点间的值是否相同;
我们可以看到都是void*类型的参数,意味着Redis的链表可以多态的表示,即一条链表上的节点数据类型不一定一致。

字典
字典是一种用于保存键值对的抽象数据结构,在字典中一个键关联着一个值。Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个节点,每个节点存储着字典的键值对。因此先来看看哈希表结构:

//哈希表结构
typedef struct dictht {
	//存储节点的数组
	dictEntry **table;
	//记录了哈希表的大小
	unsigned long size;
	//哈希表的掩码,为size大小-1,用于计算索引值,
	unsigned long sizemask;
	//哈希表已有的节点个数
	unsigned long used;
}
//哈希表节点
typedef struct dictEntry {
	void *key;
	union {
		void *val;
		uint64_tu64;
		int64_ts64;
	}v;
	//下一个节点指针,用于发生了hash冲突时转换为链表
	struct dictEntry *next;
}dictEntry;

因此我们能发现,字典底层采用的是数组+链表的结构,有点像hashmap呢。接下来看看字典结构:

typedef struct dict {
	//特定函数类型
	dictType *type;
	//私有数据,一般作为函数的参数
	void *privdata;
	//两个哈希表
	dictht ht[2];
	//rehash的值,不再进行时为-1;
	in trehashidx;
}dict;
typedef struct dictType {
	//计算hash值的函数
	unsigned int (*hashFunction)(const void *key);
	//复制键的函数
	unsigned *(*keyDup) (void *privdata, const void *key);
	//复制值的函数
	unsigned *(*valDup) (void *privdata, const void *obj);
	//对比键的函数
	int (*keyCompare) (void *privdata, const void *key1, const void *key2);
	//销毁键的函数
	void (*keyDestructor)(void *privdata,void *key);
	//销毁值的函数
	void (*valDestructor)(void *privdata,void *obj);
}dictType;

看了以上的代码和注释,我们可能会好奇,为什么哈希表是一个容量为2的数组,心的我们会发现还有一个rehashidx,没错,有一个数组单元就是我们用来进行扩容/缩小时会用到的哈希表。

再来看看一个键值对时如何存储到哈希表中的,熟悉hashmap应该很快可以理解。首先利用hash函数计算出key的hash值,然后和哈希掩码作&操作,散列到某一索引处。若索引处为空,直接放置数据即可;若发生了冲突,则采用头插法插入数据。
随着节点的不断增加,性能会随之下降。为了使负载因子在一个合理的范围,对其进行rehash。步骤如下:
·为ht[1]分配空间:
·若为扩展操作,大小为>=ht[0]的used(哈希表中的属性)*2的最小二次幂;
·若为收缩操作,大小为>=ht[0]的used的最小二次幂
·将ht[0]中的键值重新计算,散列到ht[1]上;
·将ht[0]指向ht[1];
·将ht[0];置空;

我们上面说到,将ht[0]rehash到ht[1]中,但这个过程并非一次完成的,而是渐进式的。在节点量少的时候还有可能,但假如有成千上万个节点,那的暂停多久来完成rehash?将会严重影响数据库效率。此时就会用到哈希表中的rehashidx属性。渐进式rehash的详细步骤:
·为ht[1]分配空间,字典同时拥有ht[1]和ht[0];
·将rehashidx初始化为0;
·在rehash期间,每次对程序的crud操作完成后,顺带将ht[0]哈希表在rehashidx上的键值对 rehash到ht[1]上,当rehash工作完成后rehashidx+1;
·当ht[0]上的键值对都rehash到ht[1]上时代表rehash操作完成,rehashidx设为-1;
将rehash的工作分散到每一次对字典序的crud操作中,避免了集中式的rehash计算量。
另外,在rehash过程中的查找和删除操作会依次遍历ht[0]和ht[1]两张哈希表,而增加操作只会往ht[1]表中执行。

最后,了解下再什么时候会进行rehash:
·服务器没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子>=1;
·服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子>=5;

负载因子: ht[0].used / size;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值