前言
首先,Redis作为一个优秀开源的内存数据结构存储系统,可以用作数据库、缓存和消息中介。它支持多种数据结构,包括字符串、哈希表、列表、集合、有序集合等。当我们谈论Redis中的“哈希表”时,我们通常是指Redis用作数据结构之一的哈希数据类型,而不是Redis内部用于存储所有键值对的全局哈希表实现。
一、什么是Redis的全局哈希表
Redis的全局哈希表是一个内部数据结构,用于存储Redis服务器中的所有键值对。全局哈希表通常是一个由哈希桶组成的数组。每个哈希桶可以保存一个或多个键值对,这些键值对通过哈希函数映射到特定的哈希桶中。当发生哈希冲突(即多个键哈希到同一个桶)时,Redis会使用链表或其他数据结构来解决冲突。
全局哈希表的优势在于它提供了一种高效的方式来存储和检索键值对。通过将键哈希到哈希桶中,Redis可以在平均常数时间内执行查找、插入和删除操作,从而实现快速的数据访问。
一个哈希表就是一个数组,数组每个元素叫哈希桶,每个哈希桶保存键值对数据。然而哈希桶中的元素不是 value 本身,而是指向 value 的指针,即 value 存储的内存地址
如图,这个哈希表保存了所有键值对,哈希桶中的 entry 元素保存key 和value 指针,哈希表能在 O(1) 时间复杂度快速查找键值对,所以我们只需要计算 key 的哈希值就能找到对应的哈希桶位置,进而找到对应的 entry 元素。不同类型的 value 都能被找到,不论是 String、List、Set、Hash。
这种查找方式只需要进行一次哈希计算,不论数据规模多少,然而,在 Redis 中写入大量数据后,操作有时候会变慢,因为出现了哈希表的冲突以及 rehash 带来的操作阻塞。
二、全局哈希表的核心实现
由于哈希表的特性,可能会出现多个键哈希到哈希表中同一个位置的情况,这称为哈希冲突。为了解决这个问题,Redis采用了链式哈希。但在某些情况下,可能还需要对整个哈希表进行rehash操作,以维持其性能。接下来,我将详细解释这些概念。
2.1. 哈希冲突
哈希冲突是指两个或更多的键通过哈希函数计算后,得到了相同的哈希值,从而它们被映射到了哈希表中的同一个位置。在理想情况下,哈希函数会将键均匀地分布在哈希表中,但实际上这是很难完全实现的,特别是在数据量非常大时。
2.2. 链式哈希
为了解决哈希冲突,Redis采用了链式哈希的方法。具体来说,哈希表中的每个位置(通常称为哈希桶或槽)实际上是一个链表或其他类型的动态数据结构(如红黑树)。当发生哈希冲突时,新的键值对会被添加到该位置的链表中。这样,哈希表中的每个位置都可以存储多个键值对,它们通过链表连接在一起。
如图所示,entry1、entry2 和 entry3 都保存在哈希桶 3 中,导致哈希冲突。entry1 增加个next 指针指向 entry2,entry2 增加next 指针指向 entry3,不论哈希桶 3 元素有多少个,都可以通过指针连接起来,形成一个链表,叫做哈希冲突链。
链式哈希会产生一个问题,随着哈希表数据越来越多,哈希冲突越来越多,单个哈希桶链表上数据越来越多,查找时间复杂度退化到 O(n),查找耗时增加,效率降低。
2.3. rehash
rehash是指重新构建哈希表的过程。当哈希表中的键值对数量与哈希表的大小(即哈希桶的数量)之间的比率变得不合适时,Redis会执行rehash操作。例如,当哈希表变得过于拥挤(负载因子过高)或过于稀疏(负载因子过低)时,就可能需要进行rehash。
rehash操作通常涉及以下步骤:
- 创建一个新的哈希表,其大小可能大于或小于当前的哈希表,具体取决于负载因子的调整需求。
- 将旧哈希表中的所有键值对重新哈希到新哈希表中。
- 一旦所有键值对都被迁移到新的哈希表中,旧的哈希表就会被释放。
在第二步涉及大量数据拷贝,如果一次性把哈希表 1 迁移完,耗时很长,会造成线程阻塞,无法处理其他请求,Redis 是怎么处理这个问题呢?它采用渐进式 rehash
2.4. 渐进式 rehash
渐进式rehash是Redis中一种特殊的rehash策略,用于在不影响服务器性能的情况下逐步完成rehash操作。由于Redis是单线程的,并且需要处理来自客户端的实时请求,因此一次性完成大规模的rehash操作可能会导致服务器在一段时间内无法响应客户端的请求。
为了避免这种情况,Redis采用了渐进式rehash的方法。这种方法将rehash操作分成多个小步骤,每个步骤只迁移一小部分键值对。这样,Redis可以在处理客户端请求的同时,逐步完成整个rehash操作。渐进式rehash通过维护两个哈希表(一个旧的和一个新的)并在处理客户端请求时逐步迁移键值对来实现。
渐进式 rehash 把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
总结来说,哈希冲突是哈希表不可避免的问题,Redis通过链式哈希来有效地解决它。而rehash和渐进式rehash则是Redis为了维持哈希表性能而采用的重要策略。
三、全局哈希表的优势
全局哈希表的优势主要体现在以下几个方面:
- 高效查找:全局哈希表通过哈希函数将键映射到存储位置,使得查找操作的时间复杂度降低到接近常数级别。这意味着无论数据量有多大,查找特定键的值所需的时间都基本保持不变,从而实现了非常快速的查找性能。
- 快速插入和删除:全局哈希表不仅支持高效的查找操作,还提供了快速的插入和删除功能。插入和删除操作的时间复杂度也接近常数级别,因为它们不涉及数据移动或重新排序等耗时操作。
- 数据共享和同步:全局哈希表可以在分布式环境中实现数据的共享和同步。多个客户端或节点可以同时读写全局哈希表,从而保持数据的一致性和实时性。这对于需要协同处理或共享数据的应用场景非常有用。
- 高可扩展性和容错性:通过将数据分布在多个节点上,全局哈希表可以很容易地进行扩展,以应对不断增长的数据量。同时,当某个节点发生故障时,其他节点可以继续提供服务,确保系统的可用性和稳定性。
需要注意的是,全局哈希表也存在一些局限性,例如无法按照特定顺序遍历元素、键的唯一性要求等。因此,在选择使用全局哈希表时,需要根据具体的应用场景和需求进行权衡和考虑。
四、集合全局哈希表来看Redis数据查询流程
在集群环境下,数据的分布和查询流程与单实例环境有所不同。Redis集群使用分片(sharding)来将数据分布在多个节点上,每个节点负责处理一部分哈希槽(hash slot)中的数据。全局哈希表的概念在这里仍然适用,但是它是分布在集群的所有节点上的。以下是Redis集群环境下的数据查询流程:
1. 客户端连接:
客户端首先连接到Redis集群中的一个节点,这个节点可以是集群中的任意节点,因为Redis集群中的每个节点都保存了集群的元数据,包括哈希槽与节点的映射关系。
2. 发送查询命令:
客户端向连接的节点发送查询命令,并指定要查询的键。
3. 计算哈希槽:
Redis集群不使用单个哈希函数直接定位到具体的节点,而是使用哈希槽的概念。Redis集群有16384个哈希槽,客户端会根据键的哈希值计算出这个键属于哪个哈希槽。哈希槽的计算公式通常是HASH_SLOT = CRC16(key) mod 16384,其中CRC16是16位循环冗余校验码。
4. 节点定位:
客户端或代理节点(如果使用了像Twemproxy这样的代理)会查询集群的元数据,确定负责处理该哈希槽的节点。如果客户端已经连接到了正确的节点,它会直接向该节点发送查询命令;否则,它会重定向到正确的节点。
5. 在节点的哈希桶查找数据:
一旦很久key的hash计算后确定了哈希桶的位置,Redis会在这个桶中查找数据。由于哈希冲突的存在,一个桶中可能存储了多个键值对。因此,Redis可能需要遍历桶中的链表或其他数据结构来找到确切的键值对。
如果桶中的数据结构是链表,Redis会遍历链表,逐个比较链表中的键与客户端提供的键是否匹配。
如果使用的是其他数据结构(如红黑树),则按照相应数据结构的查找算法进行查找。
6. 提取并返回数据:
如果找到了匹配的键值对,节点会提取出对应的值,并将其返回给客户端。如果查询的键不存在,节点会返回一个空结果或错误信息。
7. 处理特殊情况:
在查询过程中,节点可能会遇到一些特殊情况,如数据迁移、节点故障等。Redis集群有一套机制来处理这些情况,例如使用重定向命令ASK或MOVED来指引客户端到正确的节点。
8. 结束查询:
查询完成后,节点会关闭与客户端的连接(如果是一次性查询的话),或者等待处理下一个客户端请求。
五、数据库和全局哈希表的关系在Redis中,“数据库”是一个逻辑上的概念,用于对键值对进行分组和隔离。Redis服务器默认会创建16个数据库(编号从0到15),这些数据库在内部是通过数组来存储和管理的。每个Redis客户端都可以选择一个目标数据库进行操作,默认情况下,客户端的目标数据库为0号数据库,但可以通过执行SELECT命令来切换目标数据库。
每个Redis数据库都有其自己的键值对集合,这些键值对在全局范围内是隔离的。这意味着,在不同的数据库中,可以存在相同的键,它们不会相互干扰或冲突。
关于全局哈希表,它是Redis内部用于实现快速键值对访问的数据结构。Redis使用一个全局哈希表来保存所有的键值对,无论这些键值对属于哪个数据库。全局哈希表通过哈希算法将键映射到相应的哈希桶中,以实现快速的查找、插入和删除操作。
然而,需要注意的是,尽管所有数据库共享同一个全局哈希表,但它们在内部是通过不同的键值对集合来隔离的。当切换到不同的数据库时,Redis会在内部切换到相应的键值对集合,以确保操作的正确性和隔离性。
总结来说,Redis中的数据库是一个逻辑上的概念,用于对键值对进行分组和隔离。而全局哈希表是Redis内部用于实现快速键值对访问的数据结构。尽管所有数据库共享同一个全局哈希表,但它们在内部是通过不同的键值对集合来隔离的。
六、Redis的内部哈希表和集群的哈希槽的区分
全局哈希表是Redis内部用于存储所有键值对的数据结构,它是一个由哈希桶组成的数组,每个哈希桶可以保存一个或多个键值对。键通过哈希函数被映射到哈希桶中,当发生哈希冲突时(即多个键映射到同一个哈希桶),Redis会使用链表或其他数据结构来解决冲突。
而哈希槽(hash slot)是Redis集群中的一个概念,不是单实例Redis的全局哈希表实现的一部分。在Redis集群中,数据被分布在多个Redis节点上,为了提高数据的分布均匀性和可扩展性,Redis引入了哈希槽的概念。
哈希槽是一个逻辑上的分区,整个键空间被划分为16384个哈希槽,每个键都会被映射到这些哈希槽中的一个。Redis集群中的每个节点负责处理一部分哈希槽,这样可以将数据均匀地分布在多个节点上。
需要注意的是,哈希槽和Redis内部的全局哈希表是两个不同的概念,它们分别用于不同的目的。全局哈希表是Redis内部用于存储键值对的数据结构,而哈希槽是Redis集群中用于数据分布和节点间通信的逻辑分区。
总结来说,Redis的全局哈希表是一个内部数据结构,用于存储键值对,并通过哈希函数将键映射到哈希桶中。而哈希槽是Redis集群中的一个概念,用于在多个节点之间分配数据和实现数据的分布式存储。
总结
Redis的全局哈希表和查询流程是其高性能和灵活性的关键所在。通过精心设计的数据结构和算法,Redis实现了在内存中快速存储和检索数据的能力。未来,随着硬件技术的发展和新型存储介质的涌现,我们期待Redis能够进一步优化其数据组织方式,为我们带来更加出色的性能体验。