Redis 的 hash 是什么?
Redis 的 hash 是什么?
回答重点
Redis 的 hash 是一种键值对集合,可以将多个字段和值存储在同一个键中,便于管理一些关联数据。
特点如下:
- 适合存储小数据,使用哈希表实现,能够在内存中高效存储和操作。
- 支持快速的字段操作(如增、删、改、查),非常适合存储对象的属性。
扩展知识
hash 常用命令
HSET:设置 hash 中指定字段的值。
1 | HSET user:1001 name "mianshiya" |
HGET:获取 hash 中指定字段的值。
1 | HGET user:1001 name |
HMSET:一次性设置多个字段的值。
1 | HMSET user:1001 name "mianshiya" age "18" city "shanghai" |
HMGET:一次性获取多个字段的值。
1 | HMGET user:1001 name age |
HDEL:删除 hash 中的一个或多个字段。
1 | HDEL user:1001 age |
HINCRBY:为 hash 中的字段加上一个整数值。
1 | HINCRBY user:1001 age 1 |
Hash 底层实现解析
Hash 是 Redis 中的一种数据基础数据结构,类似于数据结构中的哈希表,一个 Hash 可以存储 2 的 32 次方 - 1 个键值对(约 40 亿)。底层结构需要分成两个情况:
- Redis 6 及之前,Hash 的底层是压缩列表加上哈希表的数据结构(ziplist + hashtable)
- Redis 7 之后,Hash 的底层是紧凑列表(Listpack) 加上哈希表的数据结构(Listpack + hashtable)
ziplist 和 listpack 查找 key 的效率是类似的,时间复杂度都是 O(n),其主要区别就在于 listpack 解决了 ziplist 的级联更新问题。
那么 hash 在什么时候使用 ziplist 和 listpack,什么时候使用 Hashtable 呢?
Redis 内有两个值,分别是 hash-max-ziplist-entries (hash-max-listpack-entries)以及 hash-max-ziplist-value(hash-max-listpack-value),即 Hash 类型键的字段个数(默认512)以及每个字段名和字段值的长度(默认64)。
redis 7.0 为了兼容早期的版本,并没有把 ziplist 相关的值删掉。
当 hash 小于这两个值的时候,会使用 listpack 或者 ziplist 进行存储。
当大于这两个值的时候会使用 hashtable 进行存储。
这里需要注意一个点,在使用 hashtable 结构之后,就不会再退化成 ziplist 或 listpack,之后都是使用 hashtable 进行存储。
这里需要注意,这两个值是可以修改的。如下图所示,将其值修改为 4399 以及 2024 并且查询后效果如下图所示:
聊聊 Redis 中的 Hashtable?
面试官可能会细问 Hashtable 相关的知识点。
Hashtable 就是哈希表实现,查询时间复杂度为 O(1),效率非常快。
看下 HashTable 的结构:
1 | typedef struct dictht { |
dictht 一共有 4 个字段:
- table:哈希表实现存储元素的结构,可以看成是哈希节点(dictEntry)组成的数组。
- size:表示哈希表的大小。
- sizemask:这个是指哈希表大小的掩码,它的值永远等于 size-1,这个属性和哈希值一起约定了哈希节点所处的哈希表的位置,索引的值 index = hash(哈希值) & sizemask。
- used:表示已经使用的节点数量。
我们再看下哈希节点(dictEntry)的组成,它主要由三个部分组成,分别为 key,value 以及指向下一个哈希节点的指针,其源码中结构体的定义如下:
1 | typedef struct dictEntry { |
结合以上图片,我们可以发现,哈希节点中的 value 值是由一个联合体组成的。因此,这个值可以是指向实际值的指针、64位无符号整数、64 为整数以及 double 的值。
这样设计的主要目的就是为了节省 Redis 的内存空间,当值是整数或者浮点数的时候,可以将值的数据存在哈希节点中,不需要使用一个指针指向实际的值,从一定程度上节省了空间。
渐进式 rehash
还需要了解另一个面试重点,Redis 的渐进式 rehash。
从字面意思上来说,就是一点点地扩容,而不是直接一次性完成扩容。
我们再来看这个图:
dict 有两个 dictht 组成,为什么需要 2 个哈希表呢?主要原因就是为了实现渐进式。
在平时,插入数据的时候,所有的数据都会写入 ht[0] 即哈希表 1,ht [1] 哈希表 2 此时就是一张没有分配空间的空表。
但是随着数据越来越多,当 dict 的空间不够的时候,就会触发扩容条件,其扩容流程主要分为三步:
1)首先,为哈希表 2 即分配空间。新表的大小是第一个大于等于原表 2 倍 used 的 2 次方幂。举个例子,如果原表即哈希表 1 的值是 1024,那个其扩容之后的新表大小就是 2048。
分配好空间之后,此时 dict 就有了两个哈希表了,然后此时字典的 rehashidx 即 rehash 索引的值从 -1 暂时变成 0 ,然后便开始数据转移操作。
2)数据开始实现转移。每次对 hash 进行增删改查操作,都会将当前 rehashidx 的数据从在哈希表 1 迁移到 2 上,然后 rehashidx + 1,所以迁移的过程是分多次、渐进式地完成。
注意:插入数据会直接插入到 2 表中。
3)随着操作不断执行,最终哈希表 1 的数据都会被迁移到 2 中,这时候进行指针对象进行互换,即哈希表 2 变成新的哈希表 1,而原先的哈希表 1 变成哈希表 2并且设置为空表,最后将 rehashidx 的值设置为 -1。
就这样,渐进式 rehash 的过程就完成了。
Hash 扩容以及缩容的条件
Redis 具体在什么时候会进行扩容和缩容呢?
我们先来说一下扩容,这里涉及到一个概念,即负载因子,redis 中 hash 的负载因子计算有一条公式:
1 | 负载因子 = 哈希表已保存节点的数量 / 哈希表的大小 |
Redis 会根据负载因子的情况决定是否采取扩容:
- 负载因子大于等于 1,这个时候说明空间非常紧张,新数据是在哈希节点的链表上找到的,这个时候如果服务器没有执行 RDB 快照或者 AOF 重写这两个持久化机制的时候,就会进行 rehash 操作。
- 当负载因子大于等于 5,这个时候说明哈希冲突非常严重了,这个时候无论有没有进行 AOF 重写或者 RDB 快照,都会强制执行rehash 操作。
缩容也和负载因子有关,当负载因子小于 0.1 的时候,就会进行缩容操作。这个时候新表大小是老表的 used 的最近的一个 2 次方幂。例如老表的 used = 1000,那么新表的大小就是 1024。如果没有执行 RDB 快照和 AOF 重写的时候就会进行缩容,反之不会进行。