Hash
哈希是一个键值对集合,可以将多个字段和值存储在同一个键中,便于管理一些关联数据,比如存储对象的属性。
使用场景:
商品详情:存储商品的各个属性,方便快速检索。
常用操作
hset key1:key2 field value
hget key1:key2 field
hmset key1:key2 field1 value1 field2 value2
hmget key1:key2 field1 field2
hdel key1:key2 field1
hincrby key1:key2 field num
实现
Hash类型的底层数据结构是由压缩列表或哈希表实现的:
● 如果哈希类型元素个数小于[512个(默认值,可由[hash-max-ziplist-entries)配置),所有值小于64」字节(默认值,可由[hash-max-ziplist-value」配置),Redis 会使用压缩列表作为Hash类型的底层数据结构
● 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为Hash 类型的底层数据结构。
在Redis7.0中,压缩列表数据结构已经废弃了,交由listpack数据结构来实现了。
redis中的HashTable
字典dict
- type:指向dictType指针,保存了操作特定类型键值对的函数,Redis为不同用途的字典设置不同的类型特定函数。
- privdata:保存了需要传递给不同特定函数的可选参数。
- ht[2]:两个哈希表,字典使用的哈希表是ht[0],ht[1]则是当对ht[0]哈希表进行rehash时使用。
- rehashidx:记录当前rehash进度,没有进行rehash则为-1。
哈希表dictht
● table:哈希表实现存储元素的结构,可以看成是哈希节点(dictEntry)组成的数组。
● size:表示哈希表的大小。
● sizemask:这个是指哈希表大小的掩码,它的值永远等于 size-1,这个属性和哈希值一起约定了哈希节点所处的哈希表的位置,索引l的值 index= hash(哈希值)& sizemask。
● used:表示已经使用的节点数量。
渐进式rehash
扩容条件
我们先来说一下扩容,这里涉及到一个概念,即负载因子,redis 中 hash 的负载因子计算有一条公式:
负载因子=哈希表已保存节点的数量/哈希表的大小 = ht[0].used / ht[1].size
Redis 会根据负载因子的情况决定是否采取扩容:
- 负载因子大于等于1,这个时候说明空间非常紧张,新数据是在哈希节点的链表上找到的,这个时候如果服务器没有执行RDB快照或者AOF重写这两个持久化机制的时候,就会进行rehash 操作。
- 负载因子大于等于5,这个时候说明哈希冲突非常严重了,这个时候无论有没有进行 AOF 重写或者 RDB 快照,都会强制执行rehash 操作。
缩容:
当负载因子小于 0.1的时候,就会进行缩容操作。这个时候新表大小是老表的 used 的最近的一个 2 次方幂。例如老表的 used =1000,那么新表的大小就是1024。如果没有执行RDB 快照和 AOF重写的时候就会进行缩容,反之不会进行。
扩容过程
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
- 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
- 在rehash进行期间,每次对字典执行CRUD:添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx+1(表示下次将rehash下一个桶)。
- 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash完成。
渐进式rehash的好处:
在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash 而带来的庞大计算量。
在迁移过程中,读写操作是怎样的?
在迁移时,首先会从ht[0]读取数据,如果ht[0]读不到,则会去ht[1]读。
迁移过程中,新增的数据只会存在ht[1]中,而不会存放到ht[0],ht[0]只会减少不会新增。