前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Redis源码学习之字典

Redis源码学习之字典

原创
作者头像
里奥搬砖
修改于 2018-10-12 09:58:18
修改于 2018-10-12 09:58:18
1.6K00
代码可运行
举报
运行总次数:0
代码可运行
  • 字典在Redis中的应用场景

字典在Redis中可以说无处不在,核心主要是以下两个

1.Redis数据库

2.Redis哈希对象的底层实现之一

  • 字典数据结构

1.字典结构体

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//字典
type dict struct {
	//字典类型函数
	dType *dictType
	//私有数据
	privateData interface{}
	//两张哈希表,用于渐进式rehash
	ht [2]*dictht
	//rehash索引,-1代表没有执行rehash
	rehashidx int32
	//目前正在运行的安全迭代器的数量
	iterators uint32
}

在字典结构体中,包含了一组字典函数(dictType),通过封装的方法处理对应的操作,通常在字典初始化的时候对其进行配置。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//字典类型特定函数
type dictType struct {
	//计算哈希值的函数
	hashFunction func(key interface{}) uint32
	//复制键的函数
	keyDup func(privateData, key interface{}) interface{}
	//复制值的函数
	valDup func(privateData, key interface{}) interface{}
	//对比键的函数
	keyCompare func(privateData, key1, key2 interface{}) bool
	//释放键的函数
	keyDestructor func(privateData, key interface{})
	//释放值的函数
	valDestructor func(privateData, key interface{})
}

例如,计算某个key的哈希值时,就需要调用hashFunction,可见这个方法是必须配置的。在Redis中使用的是MurmurHash2算法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//计算哈希值
func (d *dict) HashKey(key interface{}) uint32 {
	return d.dType.hashFunction(key)
}

除了一组函数之外,还可以配置字典的私有数据,透传到配置函数中进行逻辑计算。

字典还包括两张哈希表,以固定长度数组的形式保存(ht字段),这里为什么是两张哈希表,我们在【渐进式Rehash】中会说道,对应的哈希表结构体如下图所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//哈希表
type dictht struct {
	//哈希表桶数组
	table []*dictEntry
	//哈希表中桶的个数
	size uint32
	//哈希表桶个数掩码,用于计算桶的索引号,总是等于size-1
	sizemask uint32
	//哈希表已有节点数量
	used uint32
}

从代码中可以看到,哈希表的table字段是一个键值对(*dictEntry)类型的slice,相当于一张哈希表包含若干个桶;size字段就代表桶的个数;sizemask等于size-1,表示桶个数掩码,与上文中计算的哈希值可以计算出某个key所属桶的索引号;used字段表示目前哈希表已有节点个数,即使是所有桶都有数据,used和size的值也是大概率不相等的,后文会解释原因。

在最底层就是键值对了,结构体如下图所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//键值对节点
type dictEntry struct {
	//键
	key interface{}
	//值
	value interface{}
	//指向下个哈希表节点,形成单链表
	next *dictEntry
}

结构很简单,key和value分别代表键和值,使用接口类型也可以体现其多态性。next字段指向下一个键值对节点,从而每个桶中存放的就是一个键值对类型的单链表了,这里也就说明了Redis处理键冲突的方法是使用【链地址法】,同时这也可以回答上文中used和size值大概率不相等的问题了。

最终,Redis字典的整体结构我们就有了整体概貌了,如下图所示,即dict>dictht>dictEntry。

我们再画的简单直白一点:

  • 字典操作

1.渐进式Rehash

首先,什么是Rehash?Rehash顾名思义,就是重新计算原有key的哈希值,重新将其分配到对应的桶里。那么为什么要做Rehash?主要是因为随着对于某个字典的不断操作,键值对的数量逐步的增多或者减少,使得哈希表的负载因子主键的偏离最优范围,导致哈希表性能降低,所以为了使哈希表的负载因子能够维持在一个合理范围,就需要Rehash了。

那么,Rehash就Rehash好了,为什么还要叫【渐进式】呢?这就是Redis设计的哈希表的核心所在了,Redis由于单线程的模型,不会因为某张哈希表负载因子偏离正常范围而一次性把整张哈希表进行Rehash操作,因为这样会阻塞住其他操作,导致这一段时间内服务不可用,所以采用了所谓的【渐进式】Rehash,也就是在对某张需要Rehash的字典执行某些操作的时候,以桶为单位逐步进行Rehash,直到所有的桶都执行完成来结束这次Rehash行为。

现在我们就可以解释前文中提到的dict结构中为什么有固定长度为2的哈希表数组了,我们将其称为【0号哈希表】和【1号哈希表】,在没有进行Rehash行为时,只有0号哈希表有键值对节点,而1号哈希表是空的,而一旦开始Rehash行为,0号哈希表和1号哈希表都会有节点,其中1号哈希表就是根据0号哈希表的节点个数创建出来的新的哈希表,来保证负载因子合理。当完成Rehash的时候,0号哈希表是空的,1号哈希表包含了所有的键值对,最终再把1号哈希表rename成0号哈希表,1号哈希表置空,恢复到未Rehash的状态,来准备下一次的Rehash。说了这么多,我感觉不如一张状态转移图来的实在:

此外,Redis还对Rehash的时机进行了定义:

1.在扩容层面上,常规情况下,Redis认为哈希表节点数量与哈希表节点桶的个数应该是1:1的关系,一旦0号哈希表used/size>1,则需要进行Rehash扩容;此外还有一种特殊情况,就是redis在fork出子进程执行持久化等操作时,Redis会提高哈希因子触发Rehash的门槛,即used/size>5的时候才会进行,这是因为现代操作系统对于fork系统调用都会使用COW(写时复制)技术进行优化,因此在此时进行Rehash就会增加整个系统的负担,得不偿失。

2.在缩容层面上,当used/size<0.1时就会被触发。

说了这么多,上个Rehash的底层代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//按步长进行rehash,以桶为单位,每一步都迁移某个桶里的所有哈希节点
//返回1表示rehash未完成
//返回0表示已经完成或者不在rehash进行中
func (d *dict) Rehash(step uint32) int {
	//不在rehash执行中直接返回
	if !d.IsReHashing() {
		return 0
	}

	//按步长进行迁移
	for ; step > 0; step-- {
		//首先判断0号哈希表是否还有已使用的桶,没有的话证明rehash完毕
		if d.ht[0].used == 0 {
			//永远以0号哈希表作为标准表
			d.ht[0] = d.ht[1]
			//初始化1号哈希表,用作下一次rehash
			d.ht[1].reset()
			//设置rehash表示为-1表示未执行rehash
			d.rehashidx = -1
			return 1
		}

		//如果0号哈希表的rehash索引大于表长度,说明有问题
		if d.ht[0].size < uint32(d.rehashidx) {
			d.rehashidx = 0
			return 0
		}

		//rehash没有完毕,则按rehashidx的值遍历0号哈希表中的桶
		//找到下一个不为空的桶
		for ; d.ht[0].table[d.rehashidx] == nil; d.rehashidx++ {
		}
		//获取桶里面的第一个节点
		for de := d.ht[0].table[d.rehashidx]; de != nil; {
			//需要把0号哈希表de的next节点临时保存下来
			oldNextDe := de.next
			//计算hash索引
			index := d.HashKey(de.key) & d.ht[1].sizemask
			//插入到1号哈希表的index号桶的表头
			de.next = d.ht[1].table[index]
			d.ht[1].table[index] = de
			//维护两张表的used字段
			d.ht[0].used--
			d.ht[1].used++
			//de设置为0号表的next节点
			de = oldNextDe
		}

		//将0号哈希表已迁移的桶置空
		d.ht[0].table[d.rehashidx] = nil
		//更新rehashidx,指向下一个桶
		d.rehashidx++
	}

	//走到这里证明还没有rehash完成
	return 0
}

说完渐进式Rehash,实际上整个字典最核心的地方就完事了,剩下的操作都不是很复杂了。

2.添加键值对节点

添加键值对的时候,如果字典正在进行Rehash中,则会进行单步Rehash。然后会判断添加的键值对的key是否在字典中存在,如果存在则返回错误;如果不存在就通过哈希算法和桶掩码计算出这个键值对所属的桶,并将其添加到这个桶存放的键值对链表的表头。这里需要注意的是,我们需要判断当前字典是否正在进行Rehash,如果是的话,则新的键值对会被放到1号哈希表(即扩容或缩容后的哈希表)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/*
 * 尝试将给定键值对添加到字典中
 * 最坏 T = O(N) ,平均 O(1)
 */
func (d *dict) Add(key, value interface{}) int {
	//判断key是否已经存在
	de := d.AddRaw(key)

	//如果存在则返回DICT_ERR
	if nil == de {
		return DICT_ERR
	}

	//设置值
	d.setVal(de, value)

	return DICT_OK
}

/*
 * 尝试将键插入到字典中
 * 如果键已经在字典存在,那么返回nil
 * 如果键不存在,那么程序创建新的哈希节点,
 * 将节点和键关联,并插入到字典,然后返回节点本身。
 * T = O(N)
 */
func (d *dict) AddRaw(key interface{}) *dictEntry {
	//如果字典正在rehash进程中
	//则执行单步rehash
	if d.IsReHashing() {
		d.rehashStep()
	}

	//键存在则直接返回
	if _, isExist := d.keyIndex(key); isExist {
		return nil
	}

	//为该节点分配空间
	de := &dictEntry{}
	d.setKey(de, key)

	//如果在rehash进程中,则插入1号表,否则插入0号表
	var ht *dictht
	if d.IsReHashing() {
		ht = d.ht[1]
	} else {
		ht = d.ht[0]
	}
	//计算桶的索引
	idx := d.HashKey(key) & ht.sizemask
	//插入到该桶的头部
	de.next = ht.table[idx]
	ht.table[idx] = de
	//更新节点数
	ht.used++

	return de
}

3.通过key查找某个键值对

查找某个键值对也会触发单步Rehash。然后会计算出这个key的哈希值,然后再字典中通过key比对函数进行查找,这里需要特别指出的是,只有在字典在0号表中没有找到并且字典正在Rehash中的时候,才会去1号表找这个键值对,这也体现出了0号表永远作为标准表的地位和Redis对于性能要求上的优雅与极致。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/*
 * 通过key查找entry
 */
func (d *dict) Find(key interface{}) *dictEntry {
	//未初始化的字典直接返回
	if d.ht[0].size == 0 {
		return nil
	}

	//如果允许的话,执行单步rehash
	if d.IsReHashing() {
		d.rehashStep()
	}

	//计算key对应的哈希值
	h := d.HashKey(key)

	//返回的entry
	var de *dictEntry = nil

	//在哈希表中进行查找
	for tableNum := 0; tableNum <= 1; tableNum++ {
		//计算索引值
		idx := h & d.ht[tableNum].sizemask
		//在idx这个桶中查找
		for current := d.ht[tableNum].table[idx]; nil != current; current = current.next {
			if d.CompareKeys(current.key, key) {
				de = current
				break
			}
		}

		//如果没有在rehash进程中,则无需查找1号哈希表
		if !d.IsReHashing() {
			break
		}
	}

	return de
}

4.修改某个key对应的value

相对来说比较简单,Redis首先通过Add方法尝试新添加一个节点,如果加不进去则证明key已经存在,则直接用新值覆盖原值即可。这里需要注意,源码中还专门对于value做了释放内存的操作,体现了Redis对于内存管理的精细化。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/*
 * 用新值替换key的原值
 * 如果key是新的,则返回1
 * 如果key是旧的,则返回0
 */
func (d *dict) Replace(key, value interface{}) int {
	//先尝试添加一个节点进去
	if d.Add(key, value) == DICT_OK {
		return 1
	}

	//如果节点存在,找到这个节点
	de := d.Find(key)
	//新值覆盖原值
	d.setVal(de, value)
	return 0
}

5.删除某个key

有了上面的经验,详细删除操作已经难不倒你了。需要指出的是,删除操作也会触发单步Rehash。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/*
 * 根据key删除节点
 */
func (d *dict) genericDelete(key interface{}, nofree bool) int {
	//如果字典还没有初始化,直接返回
	if d.ht[0].size == 0 {
		return DICT_ERR
	}

	//如果条件允许,执行单步rehash
	if d.IsReHashing() {
		d.rehashStep()
	}

	//通过key计算哈希值
	h := d.HashKey(key)

	//在表中进行查找
	for tableNum := 0; tableNum <= 1; tableNum++ {
		//计算桶的idx
		idx := h & d.ht[tableNum].sizemask
		//在该桶中进行查找
		var pre *dictEntry = nil
		var current = d.ht[tableNum].table[idx]
		for nil != current {
			if d.CompareKeys(current.key, key) { //found
				if pre == nil { //删除的是该桶的头节点
					d.ht[tableNum].table[idx] = current.next
				} else {
					pre.next = current.next
				}

				//释放删除节点的内存空间
				if !nofree {
					d.freeKey(current)
					d.freeVal(current)
				}
				current = nil

				//更新现有节点数
				d.ht[tableNum].used--

				return DICT_OK
			}
			//节点前进
			pre = current
			current = current.next
		}

		//只有在rehash中,才有必要查1号哈希表
		if !d.IsReHashing() {
			break
		}
	}

	//没找到该节点
	return DICT_ERR
}
  • 综述

通过对于Redis字典的源码学习,进一步对于Redis有了深刻的认识,佩服与作者编码功底的同时,也感慨其对于性能要求的极致。字典的核心在于渐进式Rehash,Redis通过以平摊复杂度的思想(实际上是一种分治思想)找到了属于自己的Rehash策略。此外,源码中还有着各种面向对象思想的体现,着实值得一读再读!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Redis字典设计详解
Redis 是一个高性能的 key-value 内存数据库,与 Memcached 只能存储字符串数据类型不一样,它支持存储的数据结构类型包括:字符串(string)、链表(lists)、哈希表(hash)、集合(set)、有序集合(zset)等。
用户7686797
2020/08/25
6020
美团针对Redis Rehash机制的探索和实践
Squirrel(松鼠)是美团技术团队基于Redis Cluster打造的缓存系统。经过不断的迭代研发,目前已形成一整套自动化运维体系,涵盖一键运维集群、细粒度的监控、支持自动扩缩容以及热点Key监控等完整的解决方案。同时服务端通过Docker进行部署,最大程度的提高运维的灵活性。分布式缓存Squirrel产品自2015年上线至今,已在美团内部广泛使用,存储容量超过60T,日均调用量也超过万亿次,逐步成为美团目前最主要的缓存系统之一。
美团技术团队
2019/03/22
1.2K0
美团针对Redis Rehash机制的探索和实践
【redis源码学习】看看redis的“哈希表”实现
就这种源码中的数据结构啊,我个人是比较推崇大家自己先看概念手写一个,能不能动咱另说,在写的过程中会领悟到很多直接看所领悟不到的细节。
看、未来
2021/12/24
4790
【Redis 系列】redis 学习 18,redis 存储结构原理 2
此处我下载的是 redis-6.2.5 版本的,xdm 可以直接下载上图中的 **redis-6.2.6 **版本,
阿兵云原生
2023/02/16
4070
跟着大彬读源码 - Redis 8 - 对象编码之字典
字典,是一种用于保存键值对的抽象数据结构。由于 C 语言没有内置字典这种数据结构,因此 Redis 构建了自己的字典实现。
北国风光
2019/08/06
6790
跟着大彬读源码 - Redis 8 - 对象编码之字典
Redis数据结构-字典
字典(dictionary), 又名映射(map)或关联数组(associative array)是一种抽象数据结构, 由一集键值对(key-value pairs)组成。
程序员酷森
2020/10/19
1.7K0
Redis数据结构-字典
Redis 源码解析:深入了解 Redis 的渐进式 rehash 算法
开始之前,这里推荐一篇《【Linux】常用指令详解一(mkdir -p、mkdir、cd +[目录名]、pwd)》文章,作者:池央 。
Lion Long
2024/11/11
1480
Redis 源码解析:深入了解 Redis 的渐进式 rehash 算法
Redis_字典[通俗易懂]
阅读本文之前要了解的两件事情,第一,Redis是一种Key-Value数据库,第二,字典是一种保存键值对的抽象数据结构。所以不难猜出字典在Redis中应用一定很广泛,实际上,Redis数据库的底层实现就是字典,对数据库的增删查改也是构建在对字典的操作上。那么想要深入理解Redis,字典的解密是不可缺少的。接下来,就让我们一层一层解开指点的面纱,看看它的真面目。 首先看看Redis中有哪些地方使用到了字典 一, 数据库键空间 Redis是一个键值对数据库server,server中的每一个数据库都是一个RedisDB结构,当中RedisDb结构的dict字典保存了数据库中的全部键值对。我们将这个字典称为键空间(key space),键空间和用户直接所见的数据库是直接相应的 二。 Expires字典 Redis数据库结构是一个RedisDb结构,有一个属性expires也是字典,这个字典中保存了数据库中全部键的过期时间,我们称这个字典叫做过期字典 以下贴出RedisDb的数据结构。加深了理解。
全栈程序员站长
2022/07/10
2100
一文搞懂Redis的渐进式rehash扩容机制
相信大家对hashMap都不陌生,其底层结构是数组加链表加红黑树(红黑树这里不展开),数组默认大小为16,通过key的hash值可以实现从键到值的快速访问。
程序员小义
2024/05/11
2.2K0
一文搞懂Redis的渐进式rehash扩容机制
Redis系列——10.字典结构
大年初五送财神,emmm,希望今年暴富,每年都是这么单纯简单的小愿望,没有一次让我实现的。
陈琛
2020/06/12
6520
Redis系列——10.字典结构
Redis Hashes 数据类型简述
Redis Hashes 是我们日常使用中比较高频的 Redis 数据类型,内部使用 Redis 字典结构存储,底层实现之一为哈希表结构。
WindWant
2020/10/28
4690
Redis Hashes 数据类型简述
Redis Hash哈希(2)
外层的哈希(RedisKV的实现)只用到了hashtable。当存储hash数据类型时,我们把它叫做内层的哈希。内层的哈希底层可以使用两种数据结构实现:
兜兜毛毛
2020/03/19
9190
《闲扯Redis七》Redis字典结构的底层实现
字典, 又称符号表(symbol table)、关联数组(associative array)或者映射(map), 是一种用于保存键值对(key-value pair)的抽象数据结构。在字典中, 一个键(key)可以和一个值(value)进行关联(或者说将键映射为值), 这些关联的键和值就被称为键值对。
大道七哥
2020/07/27
1.3K0
《闲扯Redis七》Redis字典结构的底层实现
3、Redis数据结构——字典-hashtable
字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对的抽象数据结构。
CodingCode
2021/06/06
1K0
3、Redis数据结构——字典-hashtable
面试官:说说Redis的Hash底层 我:......(来自阅文的面试题)
redis源码分析系列文章 [Redis源码系列]在Liunx安装和常见API 为什么要从Redis源码分析 String底层实现——动态字符串SDS Redis的双向链表一文全知道 前言 hello,各位小可爱们,又见面了。今天这篇文章来自去年面试阅文的面试题,结果被虐了。这一part不说了,下次专门开一篇,写下我面试被虐的名场面,尴尬的不行,全程尬聊。哈哈哈哈,话不多说,开始把。😂 今天要写Redis的Hash类型,如果有对Redis不熟悉,或者对其他数据类型感兴趣的,可以移步上面的系列文章。(最上
陈琛
2020/06/18
1.9K0
面试官:说说Redis的Hash底层  我:......(来自阅文的面试题)
《Redis设计与实现》读书笔记(二) ——Redis中的字典(Hash)
《Redis设计与实现》读书笔记(二) ——Redis中的字典(Hash) (原创内容,转载请注明来源,谢谢) 一、概述 字典,又称符号表、关联数组、映射,是一种保存键值对的抽象数据结构。每个键(key)和唯一的值(value)关联,键是独一无二的,通过对键的操作可以对值进行增删改查。 redis中字典应用广泛,对redis数据库的增删改查就是通过字典实现的。即redis数据库的存储,和大部分关系型数据库不同,不采用B+tree进行处理,而是采用hash的方式进行处理。 另外,毫无疑问,redis的hash
用户1327360
2018/03/07
1K0
《Redis设计与实现》读书笔记(二)  ——Redis中的字典(Hash)
Redis 字典结构细谈
说明:next 为指向下一个节点的指针,是我们熟悉的链表节点结构,单向链表,用于处理键哈希冲突问题。
WindWant
2020/10/27
8210
Redis 字典结构细谈
Redis数据结构——dict(字典)
字典在Redis中的作用是非常巨大的,对Redis数据库的增删改查等操作都构建在对字典的操作之上,因此,了解字典的底层实现能让我们对Redis有更深的理解。下面分4个模块讲解Redis的字典实现(基本所有实现细节和重点都会谈到):
向着百万年薪努力的小赵
2022/12/02
4420
Redis数据结构——dict(字典)
一起来学redis-redis数据结构
redis中没有直接使用C语言的字符串,而是自定义了一种名为简单动态字符串的抽象类型——SDS。我们下载redis源码,可以在src目录下找到一个sds.h的文件,打开这个文件查看它的部分代码:
六个核弹
2022/12/23
3010
一起来学redis-redis数据结构
Redis 数据结构-字典源码分析
字典这种数据结构并不是 Redis 那几种基本数据结构,但是 hash , sets 和 sorted sets 这几种数据结构在底层都是使用字典来实现的(并不仅仅是字典),现在看下它的实现原理。
Java技术编程
2020/05/21
7740
相关推荐
Redis字典设计详解
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文