面试官灵魂拷问:Redis 哈希槽与一致性哈希,你真能答对?

一 . 前言

在 Redis 集群里面主要涉及到两种 Hash 算法 :

  • 一种是一致性哈希 , 这种算法在 适用dis Cluster方案中并没有实现,主要在外部的代理模式 (Twemproxy)
  • 一种是 Slot 哈希槽算法 ,这种算法就是 Cluster 的核心算法

所以谈到这个问题的时候,不能只讲一部分。在 Redis 3.0 之前,Redis 是没有集群方案的,在这个时期实现 Redis 的 分布式 主要由 客户端自行实现。 一般的实现方式就是一致性 Hash。

而 Redis 3.0 之后 ,Redis 实现了 Cluster 集群,也就采用了相对而言更简洁的 Slot 槽方式。

下面来一步步的了解其中的变化 ,以及为什么要这样做 :👉👉👉

第一步:从单节点迈向多节点集群

此前我们探讨过,单节点的性能存在瓶颈。一旦单节点达到性能极限,构建集群便成为最为合理且经济的解决方案。

image.png

由此便引发了一系列关键问题:

  1. 单节点运行时,数据的写入与查询操作仅针对单个节点,逻辑较为简单。
  2. 构建集群后,新的挑战接踵而至:数据究竟该存储在哪个节点?是每个节点都完整存储所有数据,还是将数据分散存储?
  3. 同样在集群环境下,查询操作应指向哪个节点?显然,逐个节点进行全量查询的方式,性能过于低下,无法满足实际需求。

为有效解决这些问题,Redis提供了多种集群构建方式:

  • Sentinel哨兵模式:主节点承担读写任务,从节点仅支持读操作,哨兵节点则负责保障系统的高可用性。
    • 在此方案中,主节点与从节点均存储全量数据,适用于数据量相对较少的场景。
    • 主节点负责处理写操作,写入数据后,会按照既定流程将数据同步至从节点。
    • 当主节点出现异常时,从节点会通过选举机制被推选为新的主节点,以确保后续的写操作与数据同步能够持续进行。
  • Redis Cluster:该方案采用一致性Hash算法,后续我们将详细阐述其工作原理。
  • Twemproxy:类似于代理模式,Twemproxy负责实现数据的分片与负载均衡。
    • Twemproxy自身不具备高可用性,适用于对性能和稳定性要求相对较低的轻量级场景。

由此可见,Redis的集群方案丰富且细致,Sentinel模式在处理上述问题时,流程也较为完善。然而,当数据量过大时,哨兵模式便显得力不从心。倘若每个节点都复制上十亿的数据,无论是性能还是存储容量,都将面临巨大压力。

在这种情况下,就需要考虑采用Cluster方案。那么,一致性Hash在Cluster模式中究竟扮演着怎样的角色呢?

第二阶段:一致性哈希的演进历程

当我们拥有了数据以及多个集群节点后,一个关键问题便摆在眼前:如何将数据精准放置到对应的节点上?这需要满足以下几个核心要求:

  • 要求一:数据量庞大,数据仅能存储于一个节点。
  • 要求二:确保数据能相对均匀地分布在所有节点上。
  • 要求三:不能对查询效率产生负面影响,查询应仅针对一个集群节点进行。
  • 要求四:实现高可用性,节点宕机后能够快速恢复,并且支持集群的缩容与扩容。

2.1 数据分布方案的探索

针对要求一和要求二,即数据只能存储在一个节点,且所有数据需均匀分布于所有节点。通常,我们会采用“Hash + 取模”的方式来实现数据均匀分布。然而,这种传统方式存在明显弊端:一旦集群进行扩展或缩容操作,就会导致全量数据的重新迁移与计算,这在实际应用中是不可行的,成本过高且效率低下。

于是,一种更为优化的方案——一致性哈希应运而生。其具体实现步骤如下:

  • S1:首先构建一个Hash环。以常见的Hash算法为例,其取值范围是0到2^32 - 1 ,这就构成了一个整数环结构。
  • S2:对Redis的数据节点进行Hash计算,得到的Hash值决定了该节点在环上的映射位置,即映射到环上的一个点。
  • S3:在数据插入阶段,当有新数据到来时,对数据的Key进行Hash函数计算,同样会映射到环上的一个点。
  • S4:针对映射到环上的这个点,按照顺时针方向进行查找,找到的第一个Redis子节点,就是该Key对应的操作节点,即该数据应存储和操作的Redis子节点。
  • S5:在数据查询阶段,原理与插入一致,先对查询的Key进行Hash计算,再依据Hash值查找对应的节点 。

image.png

2.2 其次,保障数据均衡分布

上述一致性哈希方案存在明显问题,从示意图可见,节点C和节点A之间的空余空间较大,这极易导致数据不均衡现象,使得大部分数据被指向某一个特定节点,从而造成节点负载不均,影响整个集群的性能和稳定性。

为解决这一难题,一致性哈希引入了更为优化的策略——虚拟节点。在节点数量较少的情况下,为每个物理节点创建多个虚拟节点,通过巧妙的算法设计,让所有节点均匀地分布在Hash环上,进而实现数据在各个节点间的相对均衡存储与处理。

image.png

具体实现步骤如下:

  • S1:为每个物理节点生成多个虚拟节点,运用特定的分布算法,确保所有节点(包括虚拟节点和物理节点)均匀地散布在Hash环上,避免出现节点集中或空隙过大的情况。
  • S2:当数据操作请求进入系统时,若请求指向某个虚拟节点,系统会依据预先设定的映射规则,将其准确映射到实际的物理节点中进行处理。

通过这种方式,有效提升了数据分布的均衡性。当然,需要指出的是,这种均衡只是相对的,在极端情况下或面对特殊数据分布时,仍可能存在一定程度的偏差,但相较于未引入虚拟节点的方案,已经有了显著改善。

2.3 数据查询机制与节点扩容策略

数据查询的流程与数据插入流程基本一致。当请求抵达时,系统会基于请求的Key,运用相同的Hash计算方法,得出对应的目标数据节点,然后直接前往该节点进行数据查询操作。这一过程确保了查询操作的高效性和准确性,避免了不必要的节点遍历和数据检索,大大提升了查询性能。

而节点变动的处理则相对复杂。当某个节点需要下线时,系统需要对该节点内的所有数据进行妥善迁移。具体来说,会根据Hash环的轮动机制,将当前Hash范围内属于下线节点的数据,有序地迁移到Hash环中对应的下一个节点里。迁移完成后,后续的查询请求便会自动路由到新的节点进行处理,确保数据的连续性和系统的正常运行。在这一过程中,需要精确控制迁移流程,避免数据丢失或重复迁移,同时尽量减少对正在进行的业务操作的影响 。

image.png

2.4 一致性哈希的全面总结

如前文所述,一致性哈希这种方案常见于外部代理组件,像Twitter发布的Twemproxy,以及Predis - Proxy等。它能够实现数据的定位,并且在一定程度上达成数据的相对均匀分布。然而,不可忽视的是,这种方案仍存在一些局限性。

  • 复杂度与数据迁移:一致性哈希的实现相对复杂。当节点发生变化,比如新增或删除节点时,需要迁移整个节点的数据。这一过程不仅耗费大量的时间和系统资源,还可能在迁移过程中引发数据丢失或不一致的风险。
  • 数据均衡的相对性与定制困难:虽然一致性哈希可以实现数据的相对均衡,但难以针对性能较强或存储容量更大的优势节点进行特殊定制。数据的分布完全依赖于Hash算法,缺乏灵活性。尽管增加虚拟节点的数量能够在一定程度上缓解这一问题,但无法从根本上解决。
  • 抗风险能力较弱:一致性哈希的抗风险能力相对不足。一旦某个节点出现故障,由于其特性,流量会直接转移到下一个节点,这会导致数据大面积不均衡,使得单个节点承受过高的压力。若处理不当,可能引发连锁反应,影响整个系统的稳定性和性能。

基于一致性哈希存在的这些问题,我们有必要深入了解Redis的槽概念,以探寻更为优化的数据分布和集群管理方案。

第三阶段 : Redis 集群分片的方案

3.1 Slot Partitioning 槽分区算法

Redis Cluster 定义了 16384 个槽位,将这些槽位分发给所有的物理节点

image.png

槽位的计算

在Redis Cluster中,槽位的计算遵循特定规则:

  • 常规情况下,Cluster默认会基于Key执行crc16算法进行哈希计算,随后对16384取模,以此确定该Key对应的槽位。这种方式保证了数据在16384个槽位间相对均匀的分布。
  • 然而,在一些特殊业务场景下,Cluster也支持指定某个Key挂载到特定的槽位上。这一功能通过Tag实现,即预先设定好Tag与槽位的映射关系 ,当带有特定Tag的Key进入系统时,便能精准地定位到对应的槽位。

指令的请求

在指令请求方面,Cluster与一致性Hash有相似之处,槽位的选择由客户端来完成。

当客户端连接到Cluster集群时,客户端自身会获取一份集群的槽位信息。凭借这份信息,客户端能够直接定位到目标节点,快速执行指令,提升了数据操作的效率。

值得注意的是,每个集群节点也都保存着所有的槽位信息。这一设计主要有两个目的:一是在后续槽位发生变动时,节点能够依据自身保存的信息快速调整,保障系统的稳定运行;二是当客户端误将指令发送到错误的节点时,该节点可以基于本地保存的槽位信息,将指令转发到正确的节点,确保指令最终能够被正确处理 。

3.2 槽数据的迁移

在槽位迁移过程中,当前槽会处于一种特殊的过渡状态。此时,该槽的数据既存在于原节点A(处于“migrating”迁移状态),也存在于新节点B(处于“importing”导入状态)。这种情况下,Cluster客户端基于原有计算逻辑,可能会错误地认为数据仍在A节点(即产生“错误结果”) 。

数据迁移流程

  • S1:迁移开始时,源节点会一次性获取要迁移槽位的所有key列表,然后逐个对这些Key进行迁移操作,确保数据完整转移。
  • S2:原节点A此时扮演客户端的角色,向自身发送Dump命令,对需要迁移的数据进行序列化处理,将数据转化为适合传输的格式。
  • S3:完成序列化后,原节点A向目标节点B发起restore指令,在指令中携带序列化后的数据,请求目标节点B接收并恢复这些数据。
  • S4:目标节点B接收到数据后,将其妥善保存到本地,完成数据的导入操作,并向原节点A返回成功响应,告知数据接收已完成。
  • S5:原节点A在收到目标节点B的成功反馈后,将当前已迁移的Key从自己的节点中删除,释放本地存储空间,完成单个Key的迁移流程。

迁移期间数据请求处理

  • S1:当客户端发起数据请求时,首先会按照原有的定位逻辑去A节点(尽管此时A节点可能已是错误节点)请求数据。如果数据尚未迁移,仍在A节点中,A节点会直接将数据返回给客户端。
  • S2:若数据已经被迁移,A节点则会返回ASK指令,同时告知客户端B节点的地址,引导客户端前往新的存储节点获取数据。
  • S3:客户端收到ASK指令后,向B节点发起ASK访问。这一步主要是为了避免出现死循环。因为在数据迁移过程中,可能存在部分数据已迁移但槽位尚未完全创建好的情况,通过ASK访问可确保数据获取的准确性。
  • S4:完成ASK访问确认后,客户端在B节点执行之前请求的数据操作,获取最终所需的数据 。

3.3 槽思想的拓展应用

Redis Cluster作为Redis官方推出的集群方案,本质上体现了一种巧妙的分片思路。有趣的是,在外部工具领域,也存在着类似基于槽思想的解决方案。

以国产组件Codis为例,它本质上是一个代理中间件,和Twemproxy有着异曲同工之妙。Codis会将所有的key默认划分为1024个槽位(slot)。这里与客户端自行处理的方式不同,槽位的计算是在Codis内部完成的。对于使用者而言,访问Codis就如同访问单节点Redis一样便捷,无需过多关注背后复杂的分布式逻辑。

image.png

3.4 后续待深入拓展的细节

  • 槽位的计算方式:深入剖析其背后的算法逻辑,以及不同场景下的优化策略。
  • 槽位数量设定的缘由:探究为何定义这样特定数量的槽位,背后蕴含着怎样的权衡与考量。
  • 节点间信息同步机制:了解不同节点之间是如何高效、准确地同步信息,以保障集群的一致性和稳定性。
  • ……

由于时间有限,需要探讨的内容还有很多,后续我们将持续迭代更新,深入挖掘这些技术细节。

总结

受时间限制,关于Redis槽位,本文还有一些细节未能深入探讨。后续我们会着重研究槽位如此定义的原因,以及数据传输过程中的具体细节。

由于本人对相关知识的学习尚不够深入,文中内容或许存在不准确之处,诚挚欢迎各位读者批评指正,共同进步 。

文章来源: https://study.disign.me/article/202509/4.redis-hash-key.md

发布时间: 2025-02-24

作者: 技术书栈编辑