Redis中的hash是什么
Hash: 哈希,也叫散列,是一种通过哈希函数将键映射到表中位置的数据结构,哈希函数是关键,它把键转换成索引。
Redis Hash(散列表)是一种 field-value pairs(键值对)集合类型,类似于 Python 中的字典、Java 中的 HashMap。一个 field 对应一个value.
Hash冲突 :两个不同的key可能会映射到同一个位置去,这种问题我们叫做哈希冲突,或者哈希碰撞
Hash冲突解决方法:主要有两种两种方法,开放定址法和链地址法。
开放定址法 :
在开放定址法中所有的元素都放到哈希表里,当一个关键字key用哈希函数计算出的位置冲突了,则按照某种规则找到一个没有存储数据
的位置进行存储。这里的规则有三种:线性探测、二次探测、双重探测。
链地址法:
开放定址法中所有的元素都放到哈希表里,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储一个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成一个链表,挂在哈希表这个位置下面,链地址法也叫做拉链法或者哈希桶。
Redis 的散列表 dict 由数组 + 链表 构成,数组的每个元素占用的槽位叫做哈希桶 ,当出现散列冲突的时候就会在这个桶下挂一个链表,用"拉链法"解决散列冲突的问题。
简单地说就是将一个 key 经过散列计算均匀的映射到散列表上。
Hash数据类型底层存储数据结构实际上有两种。
1.dict结构。
2.在7.0版本之前使用ziplist,之后被listpack代替。
什么是listPack
Redis 在 5.0 新设计一个数据结构叫 listpack,目的是替代压缩列表,它最大特点是 listpack 中每个节点不再包含前一个节点的长度了,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患.
listpack 也叫紧凑列表,它的特点就是用一块连续的内存空间来紧凑地保存数据,同时为了节省内存空间,listpack 列表项使用了多种编码方式,来表示不同长度的数据,这些数据包括整数和字符串。
Redis源码对于listpack的解释为 A lists of strings serialization format,一个字符串列表的序列化格式,也就是将一个字符串列表进行序列化存储。Redis listpack可用于存储字符串或者是整型
为了避免 ziplist 引起的连锁更新问题,listpack 中的每个列表项不再像 ziplist 列表项那样,保存其前一个列表项的长度,它只会包含三个方面内容,分别是当前元素的编码类型(entry-encoding)、元素数据 (entry-data),以及编码类型和元素数据这两部分的长度 (entry-len),如下图所示。

- slen,不同于ziplist,listpackEntry 中的 len 记录的是当前 entry 的编码类型和长度,而非上一个entry的长度。
- *sval,当存储的数据为字符串时,使用该成员变量
- lval,当存储的数据为整数 ,使用该成员变量
通常情况下使用dict数据结构存储数据,每个field-va1 ue pairs构成一个dictEntry节点来保存。
只有同时满足以下两个条件的时候,才会使用listpack(7.O版本之前使用ziplist)数据结构来代替dict存储,把key-value键值对按照
field在前value在后,紧密相连的方式放到一次把每个键值对放到列表的表尾。
- ·每个键值对中的field和value的字符串字节大小都小于hash-max-listpack-value配置的值(默认64)。
- ·field-value pairs键值对数量小于hash-max-listpack-entries配置的值(默认512)。
每次向散列表写数据的时候,都会调用t_hash.c中的hashTypeConvertListpack()函数来判断是否需要转换底层数据结构。
当插入和修改的数据不满足以上两个条件时,就把散列表底层存储结构转换成dict结构。需要注意的是,不能由dict退化成listpack。
虽然使用了listpack就无法实现O(1)时间复杂度操作数据,但是使用listpack能大大减少内存占用,而且数据量比较,小,性能并不是有太大差异。
Redis数据库就是一个全局散列表。正常情况下,我只会使用ht_table[0]散列表,图2-20是一个没有进行rehash状态下的字典。
dict字典在源代码dict.h中使用dict结构体表示。
1.dictType *type
- 这是一个指向 dictType 结构的指针,dictType 通常定义了哈希表的操作接口,包括哈希函数、键值的比较函数、释放键值的函数等。这使得哈希表可以针对不同的数据类型进行定制化操作。
2.dictEntry **ht_table[2]
- 这是一个包含两个指针的数组,每个指针指向一个哈希表(ht_table[0] 和 ht_table[1])。这种设计是为了支持渐进式重新哈希(Incremental Rehashing)。当哈希表的负载因子(即 ht_used[0] / ht_table[0] 的大小)超过某个阈值时,Redis 会开始将数据从 ht_table[0] 重新哈希到 ht_table[1]。这种渐进式重新哈希可以避免一次性重新哈希带来的性能问题。
- dictEntry是哈希表中存储键值对的结构,通常包含键、值和指向下一个哈希冲突的指针。
3.unsigned long ht_used[2]
- 这是一个包含两个元素的数组,分别记录 ht_table[0] 和 ht_table[1] 中存储的键值对数量。ht_used[0] 表示 ht_table[0] 中的元素数量,ht_used[1] 表示 ht_table[1] 中的元素数量。
4.long rehashidx
- 这是一个索引值,用于记录当前重新哈希操作的进度。当哈希表正在进行重新哈希时,rehashidx 表示当前正在处理的桶索引。如果 rehashidx 为 -1,表示没有正在进行的重新哈希操作。
5.int16_t pauserehash
- 这是一个标志位,用于控制重新哈希操作是否暂停。在某些情况下,Redis 可能需要暂停重新哈希操作,以避免对性能的影响。例如,当 Redis 正在执行 RDB(Redis Database Backup)操作时,可能会暂停重新哈希。
6.signed char ht_size_exp[2]
- 这是一个包含两个元素的数组,分别记录 ht_table[0] 和 ht_table[1] 的大小指数。Redis 的哈希表大小通常是 2 的幂次方,ht_size_exp[0] 和 ht_size_exp[1] 分别表示 ht_table[0] 和 ht_table[1] 的大小指数。例如,如果 ht_size_exp[0] 为 4,则 ht_table[0] 的大小为 24=16。
·*key指针指向键值对中的键,实际上指向一个SDS实例。
虽然Redis是用c实现的,但是他们的string结构并不相同,Redis中的字符串是可以修改的字符串,在内存中它是以字节数组的形式存在的Redis 的。字符串叫sds,也就是 Simple Dynamic String,是一个带长度信息的字节数组
·v是一个uon联合体,表示键值对中的值,同一时刻只有一个字段有值,用联合体的目是节省内存。
- ·val 如果值是非数字类型,那就使用这个指针存储。
- ·uint64tu64, 值是无符号整数的时候使用这个字段存储。
- ·int64ts64, 值是有符号整数时,使用该字段存储。
- ·dob1ed, 值是浮点数是,使用该字段存储。
*next指向下一个节点指针,当散列表数据增加,可能会出现不同的key得到的哈希值相等,也就是说多个key对应在一个哈希桶里面,这就是哈希冲突。Redis使用拉链法,也就是用链表将数据串起来。
为什么说v是一个uo联合体,表示键值对中的值,同一时刻只有一个字段有值,用联合体的目是节省内存?
1.节省内存
- union 联合体是一种特殊的数据结构,它允许在同一个内存位置存储不同的数据类型。所有成员共享同一块内存,因此在任何时刻,union 中只有一个成员有值。写入一个成员会覆盖其他成员的内容.
- 在 dictEntry 中,v 的大小由其最大的成员决定。例如,void *val、uint64_t u64、int64_t s64 和 double d 的大小都是 8 字节(在 64 位系统中)。因此,v 的大小也是 8 字节,无论存储哪种类型的数据,都不会占用额外的内存。
2.灵活存储不同类型的数据
- Redis 的哈希表需要存储多种类型的数据,包括指针、整数和浮点数。通过使用 union,v 可以灵活地存储这些不同类型的数据,而不需要为每种类型分配单独的内存空间。
- 这种设计使得 dictEntry 能够高效地处理不同类型的数据,同时保持内存占用的最小化。
为啥ht_table[2]存放了两个指向散列表的指针?用一个散列表不就够了吗?
默认使用ht_tab:1e[0]进行读写数据,当散列表的数据越来越多的时候,哈希冲突严重会出现哈希桶的链表比较长,导致查询性能下降。
为了唯快不破想了一个法子,当散列表保存的键值对太多或者太少的时候,需要通过「rehash(重新散列)对散列表进行扩容或者缩容。
扩容和缩容
1.为了高性能,减少哈希冲突,会创建一个大小等于ht_used[0]*2的散列表ht_tab1e[1],也就是每次扩容时根据散列表ht_tab1e[0]已使用空间扩大一倍创建一个新散列表ht_tab1e[1]。反之,如果是缩容操作,就根据ht_tab1e[0]已使用空间缩小一倍创建一个新的散列表。
2.重新计算键值对的哈希值,得到这个键值对在新散列表ht_tab1e[1]的桶位置,将键值对迁移到新的散列表上。
3.所有键值对迁移完成后,修改指针,释放空间。具体是把ht_tab1e[0]指针指向扩容后的散列表,回收原来小的散列表内存空间,ht_tab1e[1]指针指向NULL,为下次扩容或者缩容做准备。
什么时候会触发扩容?
1.当前没有执行BGSAVE或者BGREWRITEAOF命令,同时负载因子大于等于1。也就是当前没有RDB子进程和AOF重写子进程在工作,毕竞这俩操作还是比较容易对性能造成影响的,就不扩容火上浇油了。
2.正在执行BGSAVE或者BGREWRITEA0F命令,负载因子大于等于5。(这时候哈希冲突太严重了,再不触发扩容,查询效率太慢了)。负载因子=散列表存储dictEntry节点数量/散列表桶个数。完美情况下,每个哈希桶存储一个dictEntry节点,这时候负载因子=1。
BGSAVE 和 BGREWRITEAOF 是 Redis 中的两个重要命令,它们分别用于实现 Redis 的两种持久化机制:RDB(Redis Database Backup) 和 AOF(Append Only File)。这两种机制用于将内存中的数据持久化到磁盘,以防止数据丢失。
BGREWRITEAOF 命令触发一个后台进程(子进程)来重写 AOF 文件,以压缩和优化 AOF 文件的大小,同时避免阻塞主进程。
需要迁移数据量很大,rehash操作岂不是会长时间阻塞主线程?
为了防止阻塞主线程造成性能问题,我并不是一次性把全部的key迁移,而是分多次,将迁移操作分散到每次情求中,避免集中式rehash造成长时间阻塞,这个方式叫渐进式rehash。
在执行渐进式rehash期间,dict会同时使用ht_table[0]和ht_table[1]两个散列表,rehash具体步如下:
1.将rehashidxi设置成0,表示rehash开始执行。
2.在rehash期间,服务端每次处理客户端对dict散列表执行添加、查找、删除或者更新操作时,除了执行指定操作以外,还会检查当前dict是否处于rehash状态,是的话就把散列表ht_table[0]上索引位置为rehashidx的桶的链表的所有键值对rehash到散列表ht_table[1]上,这个哈希桶的数据迁移完成,就把rehashidx的值加1,表示下一次要迁移的桶所在位置。
3.当所有的键值对迁移完成后,将rehashidxi设置成-1,表示rehash操作已完成。
rehash过程中,字典的删除、查找、更新和添加操作,要从两个ht_table都搞一遍吗?
删除、修改和查找可能会在两个散列表进行,第一个散列表没找到就到第二个散列表进行查找。但是增加操作只会在新的散列表上进行。
如果请求比较少,岂不是会很长时间都要使用两个散列表?
在Redis Server园初始化时,会注册一个时间事件,定时执行serverCron函数,其中包含rehash操作用于辅助迁移,避免这个问题。
serverCron函数除了做rehash以外,主要处理如下工作。
- 过期key删除。
- 监控服务运行状态。
- 更新统计数据。
- 渐进式rehash。
- 触发BGSAVE/AOF rewrite以及停止子进程。
- 处理客户端超时。
- ...
扩容过程:
在 Redis 的哈希表(Hash)rehash 过程中,数据迁移是分步骤、渐进式进行的,这样做是为了避免一次性迁移大量数据造成的服务器阻塞。以下是 rehash 过程中数据迁移的具体步骤:
1. 创建新的哈希表
当触发 rehash 条件时(例如,负载因子超过设定阈值),Redis 会创建一个新的哈希表 ht[1],其大小通常是 ht[0](当前哈希表)大小的两倍。
2. 初始化 rehash 索引
在 dict 结构中,rehashidx 被初始化为 0。这个索引用于跟踪 rehash 过程中已经迁移的桶的数量。
3. 渐进式迁移
Redis 使用一个定时任务或者在处理命令时的空闲时间来执行 rehash 操作。在每次 rehash 操作中,会迁移一定数量的桶(通常是一个或几个),而不是一次性迁移所有桶。具体步骤如下:
- 从 ht[0] 中取出一个桶(通过 rehashidx 定位)。
- 遍历桶中的所有 dictEntry(如果有链表,则遍历链表中的所有条目)。
- 重新计算每个 dictEntry 的哈希值,并将其插入到 ht[1] 中对应的桶中。
- 更新 rehashidx,指向下一个需要迁移的桶。
4. 更新 rehash 状态
每次迁移一定数量的桶后,Redis 会更新 rehashidx,使其指向下一个桶。这样,下一次 rehash 操作可以从上次停止的地方继续。
5. 完成 rehash
当 rehashidx 超过 ht[0] 的大小时,表示所有桶都已迁移到 ht[1]。此时,Redis 会进行以下操作:
- 将 ht[1] 设置为新的 ht[0],即让新的哈希表成为当前使用的哈希表。
- 释放旧的 ht[0] 占用的内存。
- 将 rehashidx 重置为 -1,表示 rehash 操作已完成。
6. 处理命令时的 rehash
在 rehash 过程中,Redis 仍然可以处理客户端的命令。当命令涉及到哈希表操作时,Redis 会同时在 ht[0] 和 ht[1] 中查找或插入数据,确保数据的一致性。
通过这种渐进式的 rehash 机制,Redis 能够在不影响服务器性能的情况下,平滑地完成哈希表的扩容操作。
BGSAVE和BGREWRITEAOF是什么意思?
BGSAVE 和 BGREWRITEAOF 是 Redis 中的两个重要命令,它们分别用于实现 Redis 的两种持久化机制:RDB(Redis Database Backup) 和 AOF(Append Only File)。这两种机制用于将内存中的数据持久化到磁盘,以防止数据丢失。
1. BGSAVE(RDB 持久化)
RDB 持久化
-
定义:RDB 是一种快照持久化机制,它会定期将内存中的数据集快照保存到磁盘上的一个 RDB 文件中。
-
优点:
- 数据恢复速度快:RDB 文件是一个紧凑的二进制文件,恢复数据时速度较快。
- 适合全量备份:适合用于全量数据备份,可以方便地将数据备份到其他存储介质或远程服务器。
-
缺点:
-
数据丢失风险:如果 Redis 服务器在两次快照之间发生故障,可能会丢失最后一次快照之后的数据。
-
阻塞风险:生成 RDB 文件的过程可能会阻塞主进程,尤其是在数据量较大时。
BGSAVE 命令
-
作用:触发一个后台进程(子进程)来生成 RDB 文件,而主进程继续处理客户端请求,避免阻塞。
-
执行过程:
a.主进程创建一个子进程。
b.子进程将当前内存中的数据集快照写入到一个新的 RDB 文件中。
c.子进程完成写入后,用新生成的 RDB 文件替换旧的 RDB 文件。
-
触发方式:
- 手动触发:可以通过命令 BGSAVE 手动触发。
- 自动触发:Redis 会根据配置的策略自动触发 BGSAVE。例如,可以配置 Redis 每隔一定时间或在数据变更达到一定数量时自动执行 BGSAVE。
2. BGREWRITEAOF(AOF 持久化)
AOF 持久化
- 定义:AOF 是一种日志持久化机制,它会将每个写操作命令追加到一个日志文件(AOF 文件)中。
- 优点:
- 数据安全性高:AOF 文件记录了每个写操作,即使 Redis 服务器发生故障,也可以通过重放 AOF 文件中的命令恢复数据。
- 可配置性强:可以配置不同的同步策略(如每秒同步一次、每次写操作同步一次等),以平衡性能和数据安全性。
- 缺点:
- 文件体积大:AOF 文件会不断增长,可能会占用大量磁盘空间。
- 恢复速度慢:AOF 文件是文本格式,恢复数据时速度相对较慢。
BGREWRITEAOF 命令
-
作用:触发一个后台进程(子进程)来重写 AOF 文件,以压缩和优化 AOF 文件的大小,同时避免阻塞主进程。
-
执行过程:
a.主进程创建一个子进程。
b.子进程读取当前内存中的数据集,并将其转换为一系列优化后的写操作命令,写入到一个新的 AOF 文件中。
c.子进程完成写入后,用新生成的 AOF 文件替换旧的 AOF 文件。
-
触发方式:
- 手动触发:可以通过命令 BGREWRITEAOF 手动触发。
- 自动触发:Redis 会根据配置的策略自动触发 BGREWRITEAOF。例如,可以配置 Redis 在 AOF 文件大小达到一定比例时自动执行 BGREWRITEAOF。
总结
- BGSAVE:用于生成 RDB 文件,适合全量数据备份,恢复速度快,但可能会丢失最后一次快照之后的数据。
- BGREWRITEAOF:用于重写 AOF 文件,优化 AOF 文件的大小,数据安全性高,但文件体积大,恢复速度相对较慢。
这两种持久化机制可以单独使用,也可以结合使用,具体选择取决于应用场景和对数据安全性的要求。