ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

Redis基础数据结构——字典

2021-05-31 19:06:16  阅读:152  来源: 互联网

标签:hash redis Redis ht key entry 数据结构 字典


Redis基础数据结构——字典

redis的字典hash相当于Java里面的HashMap,实现结构上也与Java的HashMap一样,都是“数组+链表”的二维结构。
不同的是,redis的字典的值只能是字符串,并且它们rehash的方式也不一样,相较于Java中HashMap的一次性rehash,redis中是渐进式rehash。
hash结构用来存储用户信息时,与字符串需要一次性全部序列化整个对象不同,hash可以对用户结构中的每个字段单独存储,这样当我们需要获取用户信息时可以进行部分获取。
1.redis字典的基本操作

hset key field value #给字典key加入field-value键值对,若field已存在,则更新field对应的value
hget key field #获取字典key中field对应的value值
hgetall key #获取字典中所有键值对
hlen key #获取字典的元素数
hmset key field value [field value …] #批量创建

在这里插入图片描述
若是value为数值,也可以进行计数

hincrby key field increment #字典key中field对应的数值value增加increment
在这里插入图片描述
2.redis字典的内部实现
字典是redis服务器中出现最频繁的复合型数据结构,除了hash结构的数据会用到字典外,整个redis数据库的所有key和value也组成了一个全局字典,还有待过期时间的key集合也是一个字典。zset集合中存储value和score值的映射关系也是字典结构。

struct RedisDb {
	dict* dict;			//全局字典
	dict* expires;		//过期字典
	...
}
struct zset {
	dict *dict;			//存放value和score的映射关系
	zskiplist *zsl;
}

既然字典在redis中这么重要,那么搞懂它的内部实现就尤为重要。

字典结构内部包含两个hashtable,通常情况下只有一个hashtable是有值的,但是在字典扩容缩容时,需要分配新的hashtable,然后进行渐进式搬迁,这时候两个hashtable存储的分别是旧的hashtable和新的hashtable。ht[0]为旧的hashtable,ht[1]为新的hashtable。等到搬迁结束之后,旧的hashtable被删除,新的hashtable取而代之。
在这里插入图片描述

struct dict {
	...
	dictht ht[2];
}

hashtable的结构和Java的HashMap几乎是一样的,都是通过分桶的方式解决hash冲突。第一维是数组,第二维是链表,数组中存储的是第二维链表的第一个元素的指针,如图所示。
在这里插入图片描述

struct dictEntry {
	void* key;
	void* val;
	dictEntry* next;
}
struct dictht {
	dictEntry** table;
	long size;			//第一维数组的长度
	long used;			//hash表中的元素个数
	...
}

3.渐进式rehash
redis的hash相较于Java的HashMap来说还有一点不同就是rehash方式不同,为渐进式rehash,那么为什么要采用这种方式呢?
因为,大字典的扩容时比较耗时的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,这是一个O(n)级别的操作,作为单线程的redis很难承受这样耗时的过程,因而redis使用渐进式rehash,虽然会慢一些,但是最终肯定是可以搬迁完的。

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing){
	long index;
	dictEntry *entry;
	dictht *ht;
	
	//这里进行小步搬迁
	if(dictIsRehashing(d))
		_dictRehashStep(d);
	
	/*获取新元素的索引,如果该元素已经存在了,则索引为-1
	 *如果新元素已经存在,则返回空指针 */
	if(
	(index = _dictKeyIndex(d, key, dictHashKey(d, key), existing)) 
	== -1)
		return NUll;
	
	/*分配内存并存储新的entry
	 *将元素插入尾部
	 *系统优先访问最近添加的entry */
	//如果字典处于搬迁过程中,要将新的元素挂接到新的数组下面
	ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
	entry = zmalloc(sizeof(*entry));
	entry->next = ht->table[index];
	ht->table[index] = entry;
	ht->used++;
	
	/*设置字典输入字段 */
	dictSetKey(d, entry, key);
	return entry;
}

字典搬迁在当前字典的后续指令中,比如来自客户端的hset、hdel等命令之后,这样稍显被动了些,除此之外,redis还会在定时任务中对字典进行主动搬迁。

//服务器定时任务
void databaseCron() {
	...
	if(server.activerehashing) {
		for(j = 0; j < dbs_per_call; j++) {
			int word_done = incrementallyRehash(rehash_db);
			if(work_done) {
				/*如果函数做了一些工作,请到此为止,
				 *将在下一个cron循环中做更多的工作*/
				 break;
			} else {
				/*如果这个db不需要rehash,
				 *将尝试rehash下一个
				 rehash_db++;
				 rehash_db %= server.dbnum;
			}
		}
	}
}

4.字典的查找过程
要想对字典进行插入,修改,删除操作都必须先要查找到元素,才可以进行数据结构的修改。因而,搞清楚hash的查找过程是很有必要的。
hashtable的元素是在第二维的链表上,所以首先要做的是确定目标key处在哪个链表上,然后再顺序遍历这个链表找到目标key。

func get(key) {
	//hash函数计算得出目标元素在数组中的位置,也就是处于哪个链表上
	let index = hash_func(key) % size;
	let entry = table[index];
	//遍历链表,找目标key
	while(entry != NULL){
		//查找成功,返回目标元素
		if(entry.key == target){
			return entry.value;
		}
		entry = entry.next;
	}
}

5.扩容和缩容
(1)扩容

//如果需要,对hash表进行扩容
static int _dictExpandIfNeeded(dict *d) {
	//若是字典正在进行rehash,则返回,此时选择不扩容
	if(dictIsRehashing(d))
		return DICT_OK;
		
	//若是hash表是空的,将其扩容至初始大小
	if(d->ht[0].size == 0)
		return dictExpand(d, DICT_HT_INITIAL_SIZE);
		
	/*如果元素数量大于等于桶的数量,
	 *且至少满足以下条件之一:
	 *1.该字典是可以进行扩容的
	 *2.元素数量/桶数量的比值大于安全阈值,默认为5
	 *此时要进行扩容,扩容为此时元素数量的两倍大小
	*/
	 if(d->ht[0].used >= d->ht[0].size &&
	 	(dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)){
	 		return dictExpand(d, d->ht[0].used*2);
	 	}
	 return DICT_OK;
}

(2)缩容
当hash表因为元素逐渐被删除变得越来越稀疏时,redis会对hash表进行缩容来减少hash表的第一维数组空间占用。缩容的条件是元素个数低于数组长度的10%,缩容不会考虑redis是否正在做bgsave。

int htNeedsResize(dict *dict) {
	long long size, used;
	size = dictSlots(dict);
	used = dictSize(dict);
	return (size > DICT_HT_INITIAL_SIZE &&
			(used*100/size < HASHTABLE_MIN_FILL));
}

标签:hash,redis,Redis,ht,key,entry,数据结构,字典
来源: https://blog.csdn.net/weixin_43559950/article/details/117406073

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有