Hash表与Dict及渐进式Rehash
1435597771 ·
Redis Hash是什么
RedisHash是Redis中的数据类型,他存储的是键值对数组。键值对数组中,每一个元素都是一个key-value成对出现。
RedisHash底层实现
RedisHash的底层实现数据结构是listpack和dict。在键值对数组个数小于512个,所有值小于64字节,那么就用listpack来存储键值对。
如果不满足上述条件,那么Redishash就会选择用Dict(也叫做哈希表,不过因为哈希表和Hash数据结构在字面上看上去是一个东西,所以就用Dict来表述哈希表,用作区分Hash数据结构)结构体来存储键值对数组。本文主要来介绍Dict结构体
Dict是什么
Dict是一个保存键值对的数据结构,他本质上是一个数组。为什么说他是数组,我们需要看Dict的结构体来说明。dict的字段有:
ht[2]:是两个dictht结构体,平时存放数据只存放进一个dictht中。
那么dictht的结构是什么样子的呢,他有几个字段需要说明
dictEntry **table: 是一个数组,数组中的元素存储的是 指向 dictEntry的指针。我们常说的hash桶就是指这个数组中的一个位置。实际的值就是存储在这个数组中元素指向的地址
size:dictht大小,表明dictEntry **table有几个元素
sizemask:dictht大小掩码,用来计算索引值
used:used是一个简单的累加计数器,代表当前哈希表里所有dictEntry` 节点(包含所有拉出来的链表节点)的绝对物理总数
dictEntry **table是什么:他是一个数组,数组中每个元素都是一个指向dictEntry的指针。
我们一直都在说dictEntry,那么他又是什么呢?
dictEntry的字段如下:
key: 就是我们往hash中存入的key
value:往hash中存入的value
next: 这个字段比较重要,他是指向下一个dictentry的指针
所以,我们通过next这个字段可以看见,dictEntry是一个单向链表。
我们结合dictEntry和dictEntry **table 可以看出来,ditch就是一个数组+单向链表的结构
数据是如何存入Dict中的
我们在上面了解了Dict的基础信息,知道了Dict是Hash数据结构的底层实现之一,那么一个键值对是如何存入Dict中的呢?
他的步骤是:
对键值对中的key进行哈希函数运算,计算出哈希值
将哈希值与dict大小取模计算,计算出来的位置,就是这个键值对在table中的位置
将键值对放入对应的哈希桶中
我们举例子再说明一下:dictEntry **table的大小为8,有key1-value1的键值对要存入其中。 hash(key1)%8 = 1 ,那么这个键值对就会被放在第一个位置。hash(key10)%8 = 2,那么就会被放入第二个位置。那么如果是hash(key9)%8=1,他是应该要存入第一个位置的对不对,那么他是怎么存放的呢?
我们说过每一个dictEntry中有next节点,key9和key1要存入一个哈希桶中,这叫做哈希冲突,那么原来桶1已经存放了key1, 现在来了key9,就新建一个节点,让桶1指向key9,然后key9的next指向key1。
这种用链表的方式解决哈希冲突,也叫做链式哈希。
现在有一个问题,如果哈希冲突严重,那么会造成单向链表过长,造成检索效率下降,那么Redis是如何解决这个问题的呢?
渐进式Rehash是什么
为了解决哈希冲突严重的问题,redis中使用渐进式Rehash的方法,我们先来了解什么是Rehash。
还记得我们说过Dict结构体中 是使用了两个dictht,只在其中一个dictht中存入数据吗?存数据的我们给他起名ht[0],那么ht[1]就是在Rehash中使用的,也就是扩容,整个过程分三步:
给ht[1]分配空间,是ht[0]的两倍大小
将ht[0]中的数据转移给ht[1]中
迁移完成,ht[0]的空间被释放,把ht[1]设置为ht[0],然后创建一个新的ht,为下一次rehash做准备
整个过程看起来很简单,但是这里面有问题,问题就在第二步中,在迁移数据的时候,会涉及大量的数据拷贝,redis又是单线程的,如果数据多,可能会对redis造成阻塞,无法服务别的请求。
为了解决这个问题,redis提出了渐进式Rehash,其实简单来说,就是不一次性迁移完所有的数据,在对数据每一次更新,新增等操作的时候,除了再执行操作之外,还会执行迁移操作。 巧妙的把数据迁移拆分成多次处理请求过程中。
同时,在数据的迁移的过程中,因为会使用到两个ht,所以更新,删除都会在这两个ht中进行操作。 总的来说,现在ht[0]中找对应的key,如果没找到就去ht[1]中找。需要特别说明的是,新增操作直接在ht[1]中操作。
负载因子
我们说了渐进式Rehash是什么,那么什么时候他会被触发呢?
在redis中提出了负载因子这个概念。还记得我们dichht中有size和used这两个字段吗?
负载因子就是used/size 。
当负载因子大于等于1,并且Redis没有在执行RDB快照或者没有进行AOF重写的时候就会进行rehash操作
当负载因此大于等于5的时候,不论redis在进行什么操作,都会强制执行rehash操作
常用命令
HSET key field value: 在存储一个哈希表(key)的键值对
HGET key field: 获取哈希表(key)中对应的field的值
HMSET key [field value...] 在一个哈希表(key)存储多个键值对
HMGET key [field...] 批量获取哈希表中的多个field对应的值
HDEL key field [field....] 删除哈希表(key)中的field键值对
HLEN key 获取哈希表中field的数量
HGETALL key 获取哈希表中所有的键值对
HINCRBY key field n 给哈希表key中的field对应的值+n
使用场景
缓存对象:与string不同的是,用hash缓存结构型的数据。而string只能缓存字符串,因此可以说明string+json 才能达成hash数据类型的操作。但是hash的优势在于,可以直接操作哈希表中的键值对