字典又称散列表,是用来存储键值(key-value)对的一种数据结构,在很多高级语言中都有实现。通常有 map 之类的。
在redis使用中的特点:
我们可以根据这几个特点来自己想一下会怎么实现。
海量数据
我们可以保存数据的指针;O(1)
取值,可以使用数组;那么连接起来就是 数组指针
。哈哈,机制!
但数组是通过下标来访问,在 redis 中它的 key 值 可不一定是整形的。所以这就需要引入 hash 函数来处理 它的 key 都可以转为整形。
Hash一般翻译为“散列”,也有直接音译为“哈希”,作用是把任意长度的输入通过散列算法转换成固定类型、固定长度的散列值,换句话说,Hash函数可以把不同键转换成唯一的整型数据。散列函数一般拥有如下特征:
那么现在应该想怎么去定位一个key 在数组中的位置?
最简单的办法是,用Hash值与数组容量取余,会得到一个永远小于数组容量大小的值,此时的值也就恰好可以当作数组下标来使用,我们把取余之后的值称为键在该字典中的索引值,即“索引值==数组下标值”,拿到“键”的索引值后,我们就知道数组中哪个元素是用来存储键值对中的“值”了。
但此方法并不是完美的,还会出现一个问题,Hash冲突。在无限的输入并在有限的输出中,肯定会发生 不同的key 定位在同一个 index 上。
为了解决Hash冲突,所以数组中的元素除了应把键值对中的“值”存储外,还应该存储“键”信息和一个next指针,next指针可以把冲突的键值对串成单链表,“键”信息用于判断是否为当前要查找的键。
类似如图所示。
在很多的编程语言中,它内置的 map 结构都是这样来设计的。
Redis的字典也是通过Hash函数来实现的,因为Redis是基于工程应用,需要考虑的因素会更多。
Redis字典实现依赖的数据结构主要包含了三部分:字典、Hash表、Hash表节点。字典中嵌入了两个Hash表,Hash表中的table字段存放着Hash表节点,Hash表节点对应存储的是键值对。
Hash表的结构体整体占用32字节,其中table字段是数组,作用是存储键值对,该数组中的元素指向的是dictEntry的结构体,每个dictEntry里面存有键值对。size表示table数组的总大小。used字段记录着table数组已存键值对个数。
sizemask字段用来计算键的索引值,sizemask的值恒等于size-1。
Hash表中元素结构体和我们前面自定义的元素结构体类似,整体占用24字节,key字段存储的是键值对中的键。v字段是个联合体,存储的是键值对中的值,在不同场景下使用不同字段。例如,用字典存储整个Redis数据库所有的键值对时,用的是*val字段,可以指向不同类型的值;再比如,字典被用作记录键的过期时间时,用的是s64字段存储;当出现了Hash冲突时,next字段用来指向冲突的元素,通过头插法,形成单链表。
Redis字典实现除了包含前面介绍的两个结构体Hash表及Hash表节点外,还在最外面层封装了一个叫字典的数据结构,其主要作用是对散列表再进行一层封装,当字典需要进行一些特殊操作时要用到里面的辅助字段。
字典这个结构体整体占用96字节,其中type字段,指向dictType结构体,里面包含了对该字典操作的函数指针,具体如下:
privdata字段,私有数据,配合type字段指向的函数一起使用。
ht字段,是个大小为2的数组,该数组存储的元素类型为dictht,虽然有两个元素,但一般情况下只会使用ht[0],只有当该字典扩容、缩容需要进行rehash时,才会用到ht[1]。
rehashidx字段,用来标记该字典是否在进行rehash,没进行rehash时,值为-1,否则,该值用来表示Hash表ht[0] 执行rehash到了哪个元素,并记录该元素的数组下标值。
iterators字段,用来记录当前运行的安全迭代器数,当有安全迭代器绑定到该字典时,会暂停rehash操作。
Hash数据类型的对象编码有两种,分别是OBJ_ENCODING_ZIPLIST和OBJ_ENCODING_HT,也就是说它的底层是由两种数据结构来支持的:一种是ZipList,另一种是哈希字典。
Redis的Hash数据类型之所以使用OBJ_ENCODING_ZIPLIST和OBJ_ENCODING_HT两种编码格式,是因为当一个Hash对象的键值对数据量比较小时,使用紧凑的数组格式可以节省内存空间。
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
hash-max-ziplist-entries的默认值为512,表示当Hash对象的键值对数量大于该值时使用OBJ_ENCODING_HT编码,否则使用OBJ_ENCODING_ZIPLIST编码。
hash-max-ziplist-value的默认值为64,表示Hash对象中的键值对存在键的长度或值的长度大于该值时使用OBJ_ENCODING_HT编码,否则使用OBJ_ENCODING_ZIPLIST编码。
Redis的Hash数据类型之所以这样设计,是因为当ZipList变得很大时会有下面几个缺点:
到这儿我们已经看完了 redis 的 hash 设计了,整体来说和其他语言的map 实现原理差不多!但是有很多的细节才是它的精髓。
欢迎关注我的公众号,分享golang日常,和学习笔记。
Go菜鸟:GolangNewBie