【Redis】集群

目录

一.集群简介

二. 数据分片算法

2.1. 哈希求余

2.1.1.MD5:一种广泛使用的哈希算法

2.1.2.哈希求余分片算法 (Hash-Based Sharding with Modulo)

2.2.一致性哈希算法

2.2.1.缺点

2.3.哈希槽分区算法

2.3.1.算法原理

2.3.2.扩容

2.3.3.问题一: Redis 集群是最多有 16384 个分片吗?

2.3.4.问题二: 为什么是 16384 个槽位?

三. 基于Docker搭建集群环境

3.1.第⼀步:创建⽬录和配置

3.2.编写generate.sh

3.3.编写docker-compose.yml

3.4.配置集群关系

3.5.使用集群

3.6.故障转移

3.6.1.主节点宕机演示

3.6.2.故障转移的过程

3.6.3.查看日志

3.7.集群扩容

3.7.1.第一步: 把新的主节点加入到集群

3.7.2.第⼆步:重新分配slots

3.7.3.第三步:给新的主节点添加从节点


一.集群简介

哨兵模式显著提升了 Redis 系统的可用性。然而,其核心数据存储仍然依赖于单个 Master 节点及其 Slave 副本。这意味着所有数据都需要完整地存储在这组主从节点上。

当数据量极其庞大,接近甚至超出 Master/Slave 节点所在服务器的物理内存容量时,系统就可能面临严重问题。

虽然硬件成本持续下降,大中型互联网公司的服务器内存容量已普遍达到 TB 级别,但在当今的“大数据”时代,1TB 内存也常常显得捉襟见肘。许多应用场景既需要存储海量数据,又要求极高的读写性能(达到内存访问速度),例如搜索引擎。此时,突破单机内存限制,获取更大的有效存储空间就变得至关重要。

解决方案的核心思路是:引入更多的机器资源。这正是“大数据”处理的核心逻辑——当单台机器无法胜任时,就通过多台机器协同工作来解决问题。

Redis Cluster(集群)模式正是基于此思路设计的。它通过引入多组 Master/Slave 节点(称为分片 Sharding),让每组节点只负责存储整个数据全集的一部分,从而构建起一个更大容量的整体存储系统。

分片原理示例:

假定整个数据集大小为 1TB。引入三组分片(每组包含一个 Master 及其 Slave):

  • 分片1: Master1 及其 Slave11、Slave12 共同保存数据集的大约 1/3。

  • 分片2: Master2 及其 Slave21、Slave22 共同保存数据集的大约 1/3(不同于分片1的数据)。

  • 分片3: Master3 及其 Slave31、Slave32 共同保存数据集的大约 1/3(不同于分片1和2的数据)。

这样,每组 Master/Slave 只需承担约 333GB 的数据存储,而非完整的 1TB。在每个分片内部,Slave 节点是其对应 Master 的实时备份,提供高可用性(当 Master 故障时,其 Slave 可以提升为新的 Master)。如果数据总量继续增长,只需简单地增加更多的分片即可线性扩展系统的总存储容量。

关于“集群”一词:

  • 广义集群: 指任何由多台机器构成的分布式系统。从这个角度看,之前介绍的主从复制搭配哨兵的模式,也可以称为一个“集群”,因为它也包含了多台协同工作的 Redis 实例。

  • 狭义集群 / Redis Cluster: 特指 Redis 提供的这种集群模式。其核心目标与哨兵模式不同:哨兵主要解决高可用性问题(主节点故障切换),而 Cluster 模式则主要解决存储空间不足的问题,通过数据分片实现存储容量的水平扩展。

关键点总结:
Redis Cluster 的核心价值在于利用数据分片 (Sharding) 机制,将庞大的数据集分散存储在多个独立的 Master/Slave 分组(分片)中。每个分片只需处理全量数据的一个子集,从而:

  1. 突破单机内存限制: 允许存储远超单机内存容量的海量数据。

  2. 保持高性能: 数据主要存储在内存中,访问速度远快于磁盘。

  3. 线性扩展: 通过增加分片数量,可以近乎线性地扩展系统的总存储容量和处理能力。

二. 数据分片算法

Redis cluster 的核⼼思路是⽤多组机器来存数据的每个部分。

那么接下来的核⼼问题就是,给定⼀个数据(⼀个具体的key),那么这个数据应该存储在哪个分⽚上?读取的时候⼜应该去哪个分⽚读取?

围绕这个问题,业界有三种⽐较主流的实现⽅式.

  1. 哈希求余
  2. 一致性哈希算法
  3. 哈希槽分区算法(Redis使⽤)

2.1. 哈希求余

2.1.1.MD5:一种广泛使用的哈希算法

MD5(Message-Digest Algorithm 5)是一种非常广泛应用的密码散列函数(Hash Function)。它的核心功能是:对输入的任意数据(例如一个字符串)进行一系列复杂的数学运算,最终生成一个固定长度的、唯一的(在理想情况下)数字摘要,通常表示为十六进制字符串(如 B2A7BB97B9B110170582)。

MD5 的主要特点:

  1. 固定长度输出:
    无论输入数据(原始字符串)有多长(一个字符还是几个GB的文件),MD5 算法计算出的哈希值(Hash Value)长度始终是固定的。具体来说,MD5 生成的是一个 128位(16字节) 的摘要,通常表示为一个 32个字符的十六进制字符串(因为每个字节需要2个十六进制字符表示)。

  2. 高度分散性(雪崩效应):
    MD5 被设计为具有强雪崩效应(Avalanche Effect)。这意味着:

    • 即使输入数据只有极其微小的差别(例如原始字符串中仅仅改动了一个字符),计算出的 MD5 哈希值也会发生巨大的、看似随机的变化

    • 两个高度相似的输入,其输出的 MD5 值会截然不同,几乎没有任何关联性。这个特性对于确保哈希值能唯一标识原始数据以及用于数据完整性校验(如文件校验)至关重要。

  3. 单向性(计算不可逆性):
    MD5 的核心特性之一是单向性(One-Way Function)

    • 正向计算容易: 给定原始字符串,可以非常快速且高效地计算出其对应的 MD5 值。

    • 反向推导困难: 给定一个 MD5 哈希值,在理论上和实践中都极其困难(近乎不可能) 通过计算反推出原始的输入字符串是什么。这是它作为密码散列函数(虽然现在已不建议用于密码存储)和数据指纹基础的重要属性。

关于“破解”MD5的说明:

网络上声称的“MD5破解”通常指的是以下情况:

  • 彩虹表(Rainbow Table)攻击: 攻击者预先计算了大量常见字符串(如字典单词、常用密码组合、短字符串)及其对应的 MD5 值,并存储在一个庞大的数据库中(即“彩虹表”)。当拿到一个 MD5 值时,就在这个数据库中查找是否存在匹配项。如果原始输入恰好是一个常见字符串且被收录在表中,就能“破解”出来。

  • 碰撞攻击(Collision Attack): 从数学上,MD5 算法已被证明存在安全漏洞,使得找到两个不同的输入生成相同的 MD5 值(即碰撞) 成为可能。但这并非根据哈希值反推出原始特定输入,而是找到任意一个能生成该哈希值的输入(可能并非原始的那个)。真正的“原像攻击”(给定哈希值找原输入)对MD5仍然非常困难。
    因此,所谓的“破解”能否成功,很大程度上依赖于原始输入的复杂度和是否被预计算过(即“随缘”)。对于强随机、长且唯一的输入,通过MD5哈希值反推其原始内容在实践中仍然被认为是不可行的。

2.1.2.哈希求余分片算法 (Hash-Based Sharding with Modulo)

哈希求余分片算法是分布式系统中常用的一种数据分片策略,其核心思想借鉴了哈希表的实现原理。

算法原理:

  1. 设定系统中总共有 N 个数据分片(Shard),通常使用连续的整数 [0, N-1] 进行编号。

  2. 在对于任意一个需要存储或查询的数据项,其唯一标识符称为 key

  3. 使用一个哈希函数(如 MD5、SHA-1 或更简单的快速哈希函数)计算该 key 的哈希值。哈希函数会将任意长度的 key 映射成一个固定长度的整数(或其二进制表示)。

  4. 将这个计算得到的哈希值对分片总数 N 进行取余运算hash_value % N

  5. 取余运算的结果(范围在 0 到 N-1 之间)即为该 key 及其对应数据应该存放(或查找)的目标分片编号

示例说明:

假设当前系统有 N=3 个分片(编号 0, 1, 2)。现有一个 key 为 "hello"

  • 计算 "hello" 的哈希值(例如使用 MD5,结果可能类似 bc4b2a76b9719d91,这本质上是一个非常大的整数)。

  • 计算 hash_value % 3。假设结果为 0

  • 那么,"hello" 及其关联的数据就会被存储(或后续查询)在 0 号分片 上。

后续任何对该 key 的查询操作,只要使用相同的哈希函数相同的 N 值进行计算,必定会得到相同的分片编号,从而能快速定位到数据所在的分片。

优点:

  • 简单高效: 计算过程直接,实现成本低。

  • 数据分布均匀: 在哈希函数特性良好且 key 空间足够随机的情况下,数据能较为均匀地分布到各个分片上,避免热点问题。

核心缺点:扩容成本高昂

分片的主要目的是提升系统的整体存储能力。随着业务增长和数据量膨胀,初始设置的分片数 N 可能不足以容纳所有数据,此时就需要进行扩容(增加新的分片,例如从 3 个增加到 4 个,N 变为 4)。

然而,N 的改变正是问题的根源:

  • 当 N 改变后,映射规则 hash(key) % N 也随之改变。

  • 对于一个固定的 key 和固定的哈希函数,其 hash(key) % N 的结果在 N 变化前后很可能不同

  • 这意味着,大量原本存储在某个分片上的数据,在扩容后根据新的 N 值计算,可能不再属于该分片,而应该迁移到另一个分片上。

数据迁移问题示例:

  1. 初始状态: 系统有 N = 3 个分片 (Shard 0, Shard 1, Shard 2)。

  2. Key 集合: 我们有 21 个不同的 key。为了简化计算,我们假设这些 key 经过哈希函数计算后,得到的哈希值 H 正好是连续的整数 100, 101, 102, ..., 120

  3. 初始分片规则: shard_id = H % 3

  4. 扩容操作: 增加一个新分片,系统变为 N' = 4 个分片 (Shard 0, Shard 1, Shard 2, Shard 3)。

  5. 新分片规则: shard_id_new = H % 4

当扩容引入一个新分片,N 变为 4 后,同样的 key 集合需要重新计算 %4。

哈希值 (H)H % 3 (旧分片)H % 4 (新分片)新旧分片是否一致?是否需要迁移?迁移说明
100100 % 3 = 1100 % 4 = 01 ≠ 0从 Shard 1 -> Shard 0
101101 % 3 = 2101 % 4 = 12 ≠ 1从 Shard 2 -> Shard 1
102102 % 3 = 0102 % 4 = 20 ≠ 2从 Shard 0 -> Shard 2
103103 % 3 = 1103 % 4 = 31 ≠ 3从 Shard 1 -> Shard 3
104104 % 3 = 2104 % 4 = 02 ≠ 0从 Shard 2 -> Shard 0
105105 % 3 = 0105 % 4 = 10 ≠ 1从 Shard 0 -> Shard 1
106106 % 3 = 1106 % 4 = 21 ≠ 2从 Shard 1 -> Shard 2
107107 % 3 = 2107 % 4 = 32 ≠ 3从 Shard 2 -> Shard 3
108108 % 3 = 0108 % 4 = 00 = 0保留在 Shard 0
109109 % 3 = 1109 % 4 = 11 = 1保留在 Shard 1
110110 % 3 = 2110 % 4 = 22 = 2保留在 Shard 2
111111 % 3 = 0111 % 4 = 30 ≠ 3从 Shard 0 -> Shard 3
112112 % 3 = 1112 % 4 = 01 ≠ 0从 Shard 1 -> Shard 0
113113 % 3 = 2113 % 4 = 12 ≠ 1从 Shard 2 -> Shard 1
114114 % 3 = 0114 % 4 = 20 ≠ 2从 Shard 0 -> Shard 2
115115 % 3 = 1115 % 4 = 31 ≠ 3从 Shard 1 -> Shard 3
116116 % 3 = 2116 % 4 = 02 ≠ 0从 Shard 2 -> Shard 0
117117 % 3 = 0117 % 4 = 10 ≠ 1从 Shard 0 -> Shard 1
118118 % 3 = 1118 % 4 = 21 ≠ 2从 Shard 1 -> Shard 2
119119 % 3 = 2119 % 4 = 32 ≠ 3从 Shard 2 -> Shard 3
120120 % 3 = 0120 % 4 = 00 = 0保留在 Shard 0
  1. 总 Key 数量: 21

  2. 无需迁移的 Key 数量: 3 

  3. 需要迁移的 Key 数量: 21 - 4 = 17

  4. 迁移比例: 17 / 21 ≈ 81%

这个具体的示例清晰地展示了哈希求余分片算法在扩容时面临的核心问题:

  1. 绝大多数数据需要迁移: 示例中高达 81% 的数据需要从一个分片移动到另一个分片。理论计算表明,当从 N 分片扩容到 N+1 分片时,平均迁移比例约为 1 - 1/(N+1)。从 3 到 4 就是 1 - 1/4 = 75%。

  2. 迁移是全局性的: 几乎所有的分片都既有数据迁出,也有数据迁入。没有哪个分片能幸免于迁移操作。

  3. 迁移成本高昂: 想象这 21 个 key 代表的是 21 亿条记录。迁移 17 亿条记录需要在网络上来回传输,消耗巨大的带宽、I/O 和 CPU 资源。迁移过程漫长且复杂,极易出错,并可能严重影响在线服务的性能和可用性。

在这种简单的示例中,21 个 key 可能只有极少数(例如 3 个)能留在原位,其余 18 个都需要迁移。在实际生产环境中,数据量往往是海量的(数百万、数亿甚至更多),这种因 N 改变导致的大规模数据迁移会带来巨大的网络传输和I/O开销,显著影响系统性能,甚至可能导致服务在迁移期间不可用或性能严重下降。

总结:

哈希求余分片算法因其简单性和均匀性被广泛采用。但其致命弱点在于扩容的代价高昂。任何分片数量 N 的变动,都会导致近乎全量数据的重新映射和潜在的大规模迁移,使得水平扩展(增加分片)成为一个复杂、耗时且资源密集的操作。在设计需要高可扩展性的系统时,需要仔细权衡此算法的适用性。

2.2.一致性哈希算法

一致性哈希算法的核心思想是为了解决传统哈希取模(直接对服务器数量取模)在分布式系统扩容或缩容时引发的数据大规模迁移问题。

它与传统取模的关键区别在于:传统取模算法是对服务器的数量进行取模,而一致性哈希算法则是对一个非常大的固定数(通常为 2^32)取模,将哈希值空间组织成一个环状结构。

其具体工作步骤如下:

  1. 构建哈希环: 一致性哈希算法首先将整个哈希值空间(0 到 2^32 - 1)想象成一个首尾相接的圆环,称为 Hash 环。这个环构成了算法定位的基础结构。

  2. 服务器节点映射: 接着,使用选定的哈希函数(如 MD5、SHA-1 或更简单的如 CRC32),对集群中的每个服务器节点进行哈希计算。通常使用服务器的 IP 地址或唯一主机名作为哈希函数的输入关键字。计算得到的哈希值决定了该服务器节点在 Hash 环上的具体位置。每个服务器节点在环上占据一个点。

  3. 数据对象定位: 当需要为一个数据对象(例如一个键 key)确定其归属的服务器时:

    • 使用步骤二中相同的哈希函数对这个数据对象的 key 进行哈希计算,得到一个哈希值。

    • 将此哈希值映射到 Hash 环上的对应位置。

    • 从这个位置开始,沿环顺时针方向移动,查找遇到的第一个服务器节点。定位到的第一个服务器节点,就是该数据对象应该被存储或访问的目标服务器。

我们看看具体的情况:

        一致性哈希算法也是使用取模的方法,但是取模算法是对服务器的数量进行取模,而一致性哈希算法是对 2^32 取模,具体步骤如下:

步骤一:一致性哈希算法将整个哈希值空间按照顺时针方向组织成一个虚拟的圆环,称为 Hash 环;

步骤二:接着,使用选定的哈希函数(如 MD5、SHA-1 或更简单的如 CRC32),对集群中的每个服务器节点进行哈希计算。从而计算从每个服务器节点在哈希环上的具体位置

步骤三:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针寻找,第一台遇到的服务器就是其应该定位到的服务器

扩容的情况

只会影响新增节点与前一个节点(新增节点逆时针查找的第一个节点)之间的数据。

缩小集群

只会影响删除节点与前一个节点(删除节点逆时针查找的第一个节点)之间的数据。

2.2.1.缺点

虽然它在分布式系统伸缩性上优势明显,但并非完美无缺,主要存在以下几个关键问题:

1.数据倾斜(负载不均衡):

  • 核心问题: 这是最突出的缺点。服务器节点在哈希环上的分布可能非常不均匀(稀疏或密集),导致数据分布和请求负载严重倾斜

  • 原因:

    • 节点数量不足: 当节点总数较少时,即使使用哈希函数,也很难保证它们在巨大的 2^32 环上均匀分布。可能出现大段环区间没有节点,而另一小段区间却聚集了多个节点。

    • 哈希函数局限性: 任何哈希函数都无法保证输入(节点标识)的输出在环上绝对均匀。

  • 后果: 落在节点稀疏区域的“大段弧”上的所有数据,最终都会被顺时针定位到同一个节点上(即下一个节点)。这会导致该节点存储的数据量远大于其他节点,接收的请求量也远高于其他节点,成为热点(Hot Spot),可能引发性能瓶颈、过载甚至宕机。

2.雪崩风险:

  • 核心问题: 某个节点宕机时,其负载并非均匀分散到所有剩余节点,而是集中转移给其在环上的下一个顺时针邻居节点

  • 原因: 原本定位到宕机节点的数据,在节点失效后,会沿着环顺时针寻找下一个可用节点,而这个节点就是宕机节点的直接后继节点。

  • 后果:

    • 该后继节点需要瞬间接管宕机节点的全部数据访问流量和可能的数据迁移(如果涉及持久化存储)。

    • 如果该后继节点本身负载已经不低,这种突发性的、集中的负载冲击极有可能导致它也被压垮,进而引发连锁反应(雪崩效应),导致整个集群服务瘫痪。

2.3.哈希槽分区算法

2.3.1.算法原理

Redis 集群真正采用的分片核心机制是哈希槽(Hash Slot)分区算法。这种算法的核心思想是将整个键空间逻辑划分为固定数量的槽位(Slot),然后将这些槽位分配给集群中的各个节点(分片)。

1. 键到哈希槽的映射

  • 对于任意一个键(Key),Redis 使用 crc16 算法计算其哈希值。
  • 然后,将此哈希值对 16384 (即 2^14, 常称为 16K) 进行取模运算:hash_slot = crc16(key) % 16384
  • 计算结果决定了该键所属的哈希槽,其值范围在 [0, 16383] 之间。这个固定的槽位数(16384)在内存开销和分布粒度之间取得了良好的平衡。

2. 哈希槽的分配

  • 这 16384 个逻辑槽位会被相对均匀地分配给集群中的各个主节点(分片)。
  • 每个节点必须明确记录自己负责管理的槽位范围或集合。
  • 分配示例(三个分片):
  • 节点 0:负责槽位 [0, 5461] (共 5462 个槽位)
  • 节点 1:负责槽位 [5462, 10923] (共 5462 个槽位)
  • 节点 2:负责槽位 [10924, 16383] (共 5460 个槽位)
  • 由于 16384 无法被 3 整除,槽位数量存在微小差异(5462 vs 5460)。然而,整体分布非常均匀,差异极小,确保了数据在各个分片上的负载基本均衡。
  • 关键点: 槽位分配具有灵活性。一个节点负责的槽位不要求必须是连续的区间,可以是离散的。例如,节点 A 可以负责槽位 1、100、5000、10000 等。只要集群配置确保每个槽位有且仅有一个负责节点,并且每一个节点负责的槽位总数大致相当即可。这种离散性为槽位的迁移和集群的弹性伸缩(如添加/移除节点)提供了便利。

3. 槽位状态管理

  • 我们上面说一个节点负责的槽位不要求必须是连续的区间,可以是离散的。那我节点怎么知道我负责哪些槽位呢?这就需要一个位图了。
  • 每个集群节点内部使用一种高效、紧凑的位图(Bitmap) 数据结构来实时表示和管理其槽位持有状态。
  • 这个位图固定包含 16384 个比特位(bit),每个比特位精确对应一个槽位号(0 到 16383)。
  • 比特位的值(0 或 1)清晰、高效地标识了当前节点是否负责管理对应的槽位:
  • 1: 表示该节点当前负责此槽位。
  • 0: 表示该节点当前不负责此槽位。
  • 使用位图进行管理的优势在于内存占用极小(仅需 16384 / 8 = 2048 字节)且查询速度极快(O(1) 时间复杂度),使得节点能够快速判断任意给定槽位是否由自己负责,这是高效处理客户端请求的关键基础。

4. 数据保存(写入)流程详解

当客户端向 Redis 集群发送一个写命令(如 SET key value)时,集群需要确保该键值对被正确地存储到负责其哈希槽的节点上。这个过程涉及客户端和节点的协作:

  1. 客户端计算目标槽位:

    • 客户端(通常是支持集群模式的 Redis 客户端库)自己使用与集群相同的算法计算键 key 的哈希槽:slot = crc16(key) % 16384

    • 客户端需要知道集群的槽位分配信息(即哪个节点负责哪个槽位)。

  2. 客户端定位目标节点:

    • 客户端在本地缓存了一份 集群槽位映射表(Cluster Slot Map)。这份映射表记录了所有 16384 个槽位当前由哪个主节点负责(包含节点 ID、IP、端口等信息)。

    • 客户端根据计算出的 slot 号,查询本地缓存的槽位映射表,找到负责该槽位的目标节点(假设为 Node X)。

    • 关键点: 客户端需要具备集群感知能力,理解槽位映射。

  3. 客户端发送命令:

    • 客户端将写命令(SET key value直接发送给它在步骤 2 中定位到的目标节点 Node X

  4. 节点验证槽位归属 (核心检查):

    • Node X 收到命令后,立即使用相同的 crc16(key) % 16384 算法计算键 key 的哈希槽 slot

    • Node X 查询其内部的 槽位状态位图

    • 情况 A:槽位归属正确 (slot 在位图中标记为 1)

      • 位图检查确认 Node X 当前确实负责槽位 slot

      • Node X 像单机 Redis 一样,正常执行该写命令(将 key-value 存入其内存数据库),并将执行结果返回给客户端。

    • 情况 B:槽位归属错误 (slot 在位图中标记为 0)

      • 位图检查发现 Node X 负责槽位 slot这表明客户端的缓存映射表可能已过期(例如,刚刚发生了槽位迁移或节点故障转移)。

      • Node X 会向客户端返回一个 MOVED 错误。这个错误信息至关重要,它包含两个关键部分:

        • MOVED <slot> <target-node-ip>:<target-node-port>

        • 例如:MOVED 1234 192.168.1.100:6381

      • 这个错误明确告诉客户端:“你请求的键属于槽位 1234,但这个槽现在不由我管了,真正负责它的节点是 192.168.1.100:6381”。

  5. 客户端处理重定向 (MOVED 错误):

    • 支持集群的客户端库在收到 MOVED 错误后,会:

      • 更新本地缓存的槽位映射表: 根据 MOVED 响应中的槽位 (<slot>) 和目标节点信息 (<target-node-ip>:<target-node-port>),刷新其缓存的映射关系。

      • 重新发送命令: 客户端根据 MOVED 响应中提供的目标节点地址,将原来的写命令(SET key value重新发送给正确的新目标节点

    • 关键点: MOVED 错误是客户端更新其槽位映射表的主要机制。智能的客户端库通常会在遇到第一个 MOVED 错误后,主动执行 CLUSTER SLOTS 命令来获取完整的、最新的集群槽位映射信息并缓存起来,以减少后续可能的 MOVED 错误。

  6. 最终存储:

    • 当命令最终被发送到正确的负责节点(负责目标槽位,且其内部位图验证通过)后,该节点执行命令,将键值对存储在其本地的内存数据库中(根据配置可能也会持久化到 RDB 或 AOF),并返回成功响应给客户端。

算法本质:

哈希槽算法巧妙地融合了一致性哈希哈希取模的思想:

  • 它首先通过哈希取模(对 16384)将键固定映射到离散的、数量庞大的逻辑槽位上(类似虚拟节点)。

  • 然后再将这些逻辑槽位灵活地分配给物理节点(类似一致性哈希中虚拟节点到物理节点的映射),并通过槽位再分配来实现集群的弹性伸缩。

2.3.2.扩容

Redis 集群的分片槽位分配规则非常灵活。以下是一个包含三个分片(0、1、2号分片)的典型初始分配方案示例:

  • 0号分片: 负责连续的槽位区间 [0, 5461],共 5462 个槽位。

  • 1号分片: 负责连续的槽位区间 [5462, 10923],共 5462 个槽位。

  • 2号分片: 负责连续的槽位区间 [10924, 16383],共 5460 个槽位。

说明: 由于总槽位数 16384 无法被 3 整除,因此各分片槽位数存在微小差异(5462 vs 5460),但这在整体上保证了数据分布的高度均衡。同时,每个分片负责的槽位不要求必须是连续的区间,也可以是离散的槽位集合(例如 [1, 100, 5000, 10000]),这为后续的弹性伸缩提供了便利。

节点内部使用高效的位图管理槽位:

每个集群节点内部维护一个固定大小的位图(Bitmap) 数据结构来表示它当前负责哪些槽位。对于 16384 个槽位:

  • 位图包含 16384 个比特位(bit),每个比特位对应一个槽位编号。

  • 位值为 1 表示节点负责该槽位,0 表示不负责。

  • 整个位图仅需 16384 bits / 8 bits per byte = 2048 bytes = 2KB 内存空间,查询效率极高(O(1) 时间复杂度),是节点快速验证键归属的核心机制。

扩容操作示例:

假设集群需要扩容,新增一个 3号分片。扩容的核心操作是对现有分片管理的槽位进行重新分配(即迁移部分槽位给新分片)。一种可能的重新分配结果如下:

  • 0号分片: 槽位区间调整为 [0, 4095],共 4096 个槽位(从原区间 [0, 5461] 迁移出部分槽位)。

  • 1号分片: 槽位区间调整为 [5462, 9557],共 4096 个槽位(从原区间 [5462, 10923] 迁移出部分槽位)。

  • 2号分片: 槽位区间调整为 [10924, 15019],共 4096 个槽位(从原区间 [10924, 16383] 迁移出部分槽位)。

  • 3号分片(新): 负责接收来自原有分片迁移出的槽位,形成离散集合:[4096, 5461](来自0号分片)、[9558, 10923](来自1号分片)、[15020, 16383](来自2号分片),总计 4096 个槽位。

关键点: 这种分配方式展示了槽位的灵活性——新分片管理的槽位来自多个原有分片,并且本身就是一个不连续的集合。目标是使每个分片最终负责的槽位数量大致相等(此例均为 4096 个)。

自动化管理:实际操作的便捷性

需要强调的是,在实际管理和运维 Redis 集群时,管理员通常无需手动指定具体的槽位编号分配给某个分片。集群管理工具(如 redis-cli --cluster 命令)提供了高级接口。管理员只需:

  1. 指定需要加入的新分片节点。

  2. 明确希望从现有分片迁移多少槽位到新分片(或者指定新分片最终应负责的目标槽位数)。

  3. 启动扩容操作。

集群管理工具会自动完成以下复杂工作:

  • 智能槽位分配: 根据请求的迁移量或目标槽位数,自动计算并从多个现有分片中选择合适的槽位集合迁移给新分片,力求最终均衡。

  • 数据迁移(Key 搬运): 对需要迁移的槽位,自动将槽位内包含的所有 Key 及其数据,在线、渐进式地从源分片迁移到目标分片(新分片),并保证迁移过程中服务可用性和数据一致性。

  • 集群状态更新: 自动更新所有节点的槽位配置信息和路由表,并通知客户端(通过 MOVED/ASK 重定向机制)更新其本地缓存。

这种声明式的操作方式(告诉集群 要做什么 而不是 具体怎么做)极大地简化了集群的扩容、缩容操作,降低了管理复杂度,是 Redis 集群设计实用性的重要体现。

2.3.3.问题一: Redis 集群是最多有 16384 个分片吗?

这个理解是不准确的。

Redis 集群的核心机制是使用 16384 个哈希槽进行数据分片,但这绝不意味着集群最多只能有 16384 个分片,更常见且合理的情况是每个分片负责管理多个哈希槽

如果集群真的配置为每个分片仅负责一个哈希槽(即达到理论上的最大分片数 16384),这将带来显著的弊端:

  1. 数据分布难以均衡: 哈希槽是数据映射的最小单位。虽然设计目标是让数据相对均匀地分布到各个槽位,但在实际应用场景中,槽位的“热度”和包含的 Key 数量必然存在差异。有的槽位可能关联大量高频访问的 Key,而有的槽位可能只有很少甚至暂时没有 Key。当每个分片只对应一个槽位时,这种 Key 数量和数据访问负载的天然不均衡性就会直接、完全地映射到分片层面。此时,很难保证各个分片承载的存储量和请求压力是均衡的,极易出现某些分片负载过高而其他分片闲置的情况,失去分片集群负载均衡的核心意义。

  2. 集群规模与可用性挑战: 构建一个包含 16000 多个分片的集群,其规模是极其庞大的,通常意味着需要成千上万台物理或虚拟服务器。如此巨大的集群规模本身就会带来严峻的挑战:

    • 运维复杂性剧增: 部署、监控、维护、故障诊断和恢复的复杂度呈指数级上升。

    • 通信开销巨大: 集群节点间需要维护状态信息和进行协调(如 Gossip 协议、故障检测、配置传播),节点数量越多,内部通信的开销(带宽、CPU、连接数)就越大,可能成为性能瓶颈。

    • 故障概率提高: 系统规模越大,单个组件(节点、网络链路)发生故障的概率就越高。虽然集群设计具有容错性,但在超大规模下,频繁的节点故障、网络分区以及由此引发的故障转移、槽位迁移等操作,会显著增加集群整体不稳定的风险,使得保证高可用性变得异常困难。系统复杂度越高,整体可靠性面临的挑战就越大。

因此,在 Redis 集群的实际部署中:

  • 分片数远小于槽位数 一个健康的 Redis 集群,其主分片数量通常会远小于 16384。每个分片负责管理一个连续的区间离散的集合(通常是连续的)的多个哈希槽(例如,几百个甚至上千个)。

  • 依赖槽位集合实现均衡: 数据在各个分片间的相对均衡性,是通过将总量大致相当的哈希槽分配给各个分片来实现的。虽然单个槽位内的 Key 数量可能有波动,但当每个分片负责足够多的槽位时,根据大数定律,Key 的总量在分片层面会趋向于均衡。槽位数量本身是固定的,分片数量是可控的,通过合理分配槽位集合就能有效控制数据分布的均衡性。

  • 官方建议控制分片规模: 正是基于对运维复杂性、通信开销和整体稳定性的考虑,Redis 作者 Antirez 明确建议,Redis 集群的分片(主节点)数量不应超过 1000 个。这个建议值是在实践经验和理论分析中找到的一个平衡点,旨在确保集群在提供水平扩展能力的同时,维持良好的性能、可管理性和可用性。将集群规模控制在这个范围内,能有效规避超大规模集群带来的诸多潜在风险。

2.3.4.问题二: 为什么是 16384 个槽位?

选择 16384 (即 2^14) 作为哈希槽的总数量,是 Redis 集群设计者在综合考虑性能、效率和实际需求后做出的一个精妙平衡。

核心原因聚焦在集群节点间通信的成本以及实际部署规模的控制上:

  1. 核心考量:集群心跳通信的带宽效率

    • Redis 集群节点间通过定期的 Gossip 协议心跳包 来交换状态信息,维持集群视图的一致性和进行故障检测。这是集群运作的基础。

    • 关键信息包含在心跳包中: 每个心跳包必须包含发送节点当前负责管理的所有哈希槽信息。这对于其他节点了解集群拓扑和数据分布至关重要。

    • 高效的位图表示: 为了在网络上高效传输槽位归属信息,Redis 使用了紧凑的位图(Bitmap) 数据结构。在这个位图中,每一位(bit)代表一个哈希槽(0 表示不负责,1 表示负责)。

    • 16384 带来的优势: 表示 16384 个槽位需要的位图大小是固定的:16384 bits / 8 bits per byte = 2048 bytes = 2KB。这是一个非常小的、几乎可以忽略不计的内存占用。

    • 对比更大的槽位数: 如果槽位数设计得更大,例如 65536 (2^16),那么表示槽位归属的位图大小将变为 65536 bits / 8 = 8192 bytes = 8KB

    • 看似不大,实则关键: 虽然 8KB 对于现代内存来说微不足道,但在高频率、集群范围广播式的心跳通信场景下,这个开销就变得非常显著:

      • 心跳包每秒会在节点间发送多次。

      • 每个节点都需要向多个(通常是所有)其他节点发送包含此位图的心跳包。

      • 在一个拥有数百个节点的集群中,额外增加的 6KB (8KB - 2KB) 数据会在网络上被反复、大量地复制和传输,累积起来会消耗可观的网络带宽,并增加节点的 CPU 处理负担(序列化/反序列化、网络 I/O)。选择 2KB 而非 8KB,显著降低了这种高频通信的基础开销,提升了集群整体的网络效率和稳定性。

  2. 实际需求与规模限制的匹配

    • 官方建议的集群规模: 如前所述,基于运维复杂性、通信开销和可用性的考虑,Redis 官方强烈建议将集群的主分片(Master)数量控制在 1000 个以内

    • 16384 的充足性: 对于最多 1000 个分片的集群来说,16384 个槽位提供了非常充裕的分片粒度

      • 平均每个分片管理 16384 / 1000 ≈ 16.384 个槽位(实际分配时数量会尽量接近)。

      • 即使每个分片管理多个槽位(通常是几十到几百个),16384 的总量也足以确保数据能被非常均匀地分配到各个分片上。

      • 这个数量级在保证数据分布均衡性的同时,避免了槽位数量过少导致分片调整(扩容/缩容)时数据迁移粒度太粗的问题。

    • 资源消耗的平衡: 16384 在提供足够粒度的同时,确保了表示槽位配置的位图体积(2KB)保持最小化,完美契合了集群心跳通信对低开销的要求。

总结:

Redis 选择 16384 个哈希槽,是一个工程上的最佳平衡点

  • 它足够大: 能够轻松满足最大建议规模(1000分片)下对数据分片粒度的需求,确保良好的负载均衡和灵活的伸缩能力。

  • 它足够小: 使得表示槽位归属的位图大小压缩到极致的 2KB,最大限度地降低了集群节点间高频、关键的心跳通信所产生的网络带宽和 CPU 资源消耗,保障了集群在大规模部署时的通信效率和整体性能。

这个设计体现了 Redis 在追求分布式能力的同时,对基础资源开销和实际部署约束的深刻理解和精细优化。

三. 基于Docker搭建集群环境

接下来基于docker,搭建⼀个集群.每个节点都是⼀个容器。

拓扑结构如下:

此处我们先创建出11个redis节点.其中前9个⽤来演⽰集群的搭建。

后两个⽤来演⽰集群扩容。

注意:本实验中是采取一台云服务器部署多个分片的。

在实际工作中,一般是通过多个主机来搭建集群的。

注意:在我们正式搭建集群之前,必须先将之前启动的redis服务器给停止掉

3.1.第⼀步:创建⽬录和配置

mkdir redis-cluster
cd  redis-cluster
touch docker-compose.yml
touch generate.sh

3.2.编写generate.sh

我们这里需要创建11个redis节点,这些redis配置文件内容其实是大同小异的,我们完全可以使用一个Shell脚本来生成

for port in $(seq 1 9); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.10${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done

for port in $(seq 10 11); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done

我们拿一部分出来讲解

脚本功能说明

for port in $(seq 1 9); \
do \
  • 循环创建:为1到9号节点创建配置(共9个节点)

mkdir -p redis${port}/
  • 创建目录:为每个节点创建独立目录(redis1/,redis2/,... redis9/)

touch redis${port}/redis.conf
  • 创建配置文件:在刚创建的目录中(redis1/,redis2/,... redis9/)创建一个空的redis.conf文件

cat << EOF > redis${port}/redis.conf
...配置内容...
EOF
  • 写入配置:使用heredoc方式向刚创建的目录(redis1/,redis2/,... redis9/)里面的redis.conf配置文件写入以下内容

核心配置解析

port 6379
  • 所有节点使用相同的服务端口(6379),实际部署时通常每个节点不同端口
bind 0.0.0.0
protected-mode no
  • 允许所有IP访问

  • 关闭保护模式(生产环境需谨慎)

appendonly yes
  • 启用AOF持久化模式
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
  • 启用集群模式

  • 集群节点配置存储文件

  • 节点故障判定超时时间5s,节点间心跳检测的最大允许间隔,超过5s长无响应即标记节点为PFail(疑似下线)

cluster-announce-ip 172.30.0.10${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
  • 节点通信配置

    • 该redis节点(自己)所在主机的IP地址:172.30.0.101 到 172.30.0.109(动态生成)

    • 客户端端口:6379

    • 集群总线端口:16379(用于节点间通信)


我们打开generate.sh把上面那个内容给复制进去,保存退出,然后执行下面这个

sh generate.sh

我们可以进去看看redis8的配置文件

现在就好了

3.3.编写docker-compose.yml

我们打开docker-compose.yml,把下面的内容粘贴进去

version: '3.7'
networks:
  mynet:
    ipam:
      config:
        - subnet: 172.30.0.0/24

services:
  redis1:
    image: 'redis:5.0.9'
    container_name: redis1
    restart: always
    volumes:
      - ./redis1/:/etc/redis/
    ports:
      - 6371:6379
      - 16371:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.101

  redis2:
    image: 'redis:5.0.9'
    container_name: redis2
    restart: always
    volumes:
      - ./redis2/:/etc/redis/
    ports:
      - 6372:6379
      - 16372:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.102

  redis3:
    image: 'redis:5.0.9'
    container_name: redis3
    restart: always
    volumes:
      - ./redis3/:/etc/redis/
    ports:
      - 6373:6379
      - 16373:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.103

  redis4:
    image: 'redis:5.0.9'
    container_name: redis4
    restart: always
    volumes:
      - ./redis4/:/etc/redis/
    ports:
      - 6374:6379
      - 16374:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.104

  redis5:
    image: 'redis:5.0.9'
    container_name: redis5
    restart: always
    volumes:
      - ./redis5/:/etc/redis/
    ports:
      - 6375:6379
      - 16375:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.105

  redis6:
    image: 'redis:5.0.9'
    container_name: redis6
    restart: always
    volumes:
      - ./redis6/:/etc/redis/
    ports:
      - 6376:6379
      - 16376:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.106

  redis7:
    image: 'redis:5.0.9'
    container_name: redis7
    restart: always
    volumes:
      - ./redis7/:/etc/redis/
    ports:
      - 6377:6379
      - 16377:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.107

  redis8:
    image: 'redis:5.0.9'
    container_name: redis8
    restart: always
    volumes:
      - ./redis8/:/etc/redis/
    ports:
      - 6378:6379
      - 16378:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.108

  redis9:
    image: 'redis:5.0.9'
    container_name: redis9
    restart: always
    volumes:
      - ./redis9/:/etc/redis/
    ports:
      - 6379:6379
      - 16379:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.109

  redis10:
    image: 'redis:5.0.9'
    container_name: redis10
    restart: always
    volumes:
      - ./redis10/:/etc/redis/
    ports:
      - 6380:6379
      - 16380:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.110

  redis11:
    image: 'redis:5.0.9'
    container_name: redis11
    restart: always
    volumes:
      - ./redis11/:/etc/redis/
    ports:
      - 6381:6379
      - 16381:16379
    command:
      redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.111

此处的端⼝映射不配置也可以,配置的⽬的是为了可以通过宿主机ip+映射的端⼝进⾏访问.通过容 器⾃⾝ip:6379的⽅式也可以访问

配置文件关键说明:

  1. 集群网络配置

    networks:
      mynet:
        ipam:
          config:
            - subnet: 172.30.0.0/24
    • 创建自定义网络 mynet

    • 使用静态IP分配(172.30.0.0/24网段)

  2. 节点通用配置

    image: 'redis:5.0.9'
    restart: always
    volumes:
      - ./redisX/:/etc/redis/
    command:
      redis-server /etc/redis/redis.conf
    • 使用Redis 5.0.9官方镜像

    • 容器异常退出时自动重启

    • 挂载宿主机目录到容器内配置文件路径

    • 启动时加载自定义配置文件

  3. 端口映射

    ports:
      - 637X:6379      # 客户端访问端口
      - 1637X:16379    # 集群总线通信端口
    • 每个节点有独立的外部端口映射

    • 保持内部端口一致(6379服务端口 + 16379总线端口)

  4. 静态IP分配

    networks:
      mynet:
        ipv4_address: 172.30.0.10X
    • 每个节点分配固定IP(101-111)

    • 确保集群节点间稳定通信

此配置将创建11个Redis节点组成的集群环境,每个节点有独立配置、数据存储和网络标识,适合用于搭建生产级Redis集群。

注意:我们这里配置的东西必须和我们前面的redis.conf文件匹配


我们把上面这个内容粘贴到

首先我们需要确保我们所有redis进程都停止掉

接下来我们就启动容器

docker-compose up -d

我们也可以看看有没有运行起来

docker ps

我们看看运行结果

root@VM-16-14-ubuntu:~/redis-cluster# docker ps
CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS          PORTS                                                                                          NAMES
4085f8d9c09e   redis:5.0.9   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   0.0.0.0:6380->6379/tcp, [::]:6380->6379/tcp, 0.0.0.0:16380->16379/tcp, [::]:16380->16379/tcp   redis10
c87a36c190ff   redis:5.0.9   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   0.0.0.0:6377->6379/tcp, [::]:6377->6379/tcp, 0.0.0.0:16377->16379/tcp, [::]:16377->16379/tcp   redis7
748d9dcf4f18   redis:5.0.9   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   0.0.0.0:6375->6379/tcp, [::]:6375->6379/tcp, 0.0.0.0:16375->16379/tcp, [::]:16375->16379/tcp   redis5
f1e68d25f634   redis:5.0.9   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   0.0.0.0:6376->6379/tcp, [::]:6376->6379/tcp, 0.0.0.0:16376->16379/tcp, [::]:16376->16379/tcp   redis6
213e6228fa4a   redis:5.0.9   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   0.0.0.0:6371->6379/tcp, [::]:6371->6379/tcp, 0.0.0.0:16371->16379/tcp, [::]:16371->16379/tcp   redis1
c96215c7c861   redis:5.0.9   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp, 0.0.0.0:16379->16379/tcp, [::]:16379->16379/tcp   redis9
ece122b66418   redis:5.0.9   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   0.0.0.0:6373->6379/tcp, [::]:6373->6379/tcp, 0.0.0.0:16373->16379/tcp, [::]:16373->16379/tcp   redis3
8929e31cee94   redis:5.0.9   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   0.0.0.0:6372->6379/tcp, [::]:6372->6379/tcp, 0.0.0.0:16372->16379/tcp, [::]:16372->16379/tcp   redis2
7a7db20f75ef   redis:5.0.9   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   0.0.0.0:6381->6379/tcp, [::]:6381->6379/tcp, 0.0.0.0:16381->16379/tcp, [::]:16381->16379/tcp   redis11
d5271e1c3347   redis:5.0.9   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   0.0.0.0:6378->6379/tcp, [::]:6378->6379/tcp, 0.0.0.0:16378->16379/tcp, [::]:16378->16379/tcp   redis8
e7c552f8dd72   redis:5.0.9   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   0.0.0.0:6374->6379/tcp, [::]:6374->6379/tcp, 0.0.0.0:16374->16379/tcp, [::]:16374->16379/tcp   redis4

当然,我们还可以通过下面这个命令来看看有没有成功

ps aux | grep redis

一共11个。

netstat -anp | grep docker

现在docker容器都已经启动了。

3.4.配置集群关系

上面创建的这11个redis节点,目前来说都是自己忙自己的,还没有形成集群,现在我们就来将它们配置成集群

此处是把前9个主机构建成集群,3主6从.后2个主机暂时不⽤.

redis-cli --cluster create \
  172.30.0.101:6379 \
  172.30.0.102:6379 \
  172.30.0.103:6379 \
  172.30.0.104:6379 \
  172.30.0.105:6379 \
  172.30.0.106:6379 \
  172.30.0.107:6379 \
  172.30.0.108:6379 \
  172.30.0.109:6379 \
  --cluster-replicas 2

命令结构解析

  1. redis-cli --cluster create

    • Redis 自带的集群管理工具

    • --cluster create 表示创建新集群

  2. 节点列表

    • 列出了 9 个 Redis 节点的地址:172.30.0.101:6379 到 172.30.0.109:6379

    • 这些节点必须已启动并准备好加入集群

  3. --cluster-replicas 2

    • 指定每个主节点有 2 个从节点

    • 这是高可用配置的关键参数

集群创建逻辑

  1. 角色自动分配

    • 工具会从 9 个节点中自动随机选出 3 个主节点(Master)

    • 剩余 6 个节点成为从节点(Replica)

    • 计算:9 节点 ÷ (1 主 + 2 副本) = 3 个主节点组

  2. 槽位分配

    • 16384 个哈希槽会被平均分配给 3 个主节点

    • 每个主节点负责约 5461-5462 个槽位

  3. 副本分配

    • 每个主节点获得 2 个副本节点

    • 副本分配遵循跨物理机原则(本例中因 IP 不同已满足)

我们赶紧去执行一下

redis-cli --cluster create 172.30.0.101:6379 172.30.0.102:6379 172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379 172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379 --cluster-replicas 2

这个时候给我们一些提示信息,其实就是先告诉我们里面的集群信息

我们来看看他给了什么信息

1. 槽位分配(Slots Allocation)

Redis 集群有 16384 个哈希槽(slots),这些槽被分配到 3 个主节点:

  • Master[0](172.30.0.101:6379):负责槽 0-5460(5461 个槽)

  • Master[1](172.30.0.102:6379):负责槽 5461-10922(5462 个槽)

  • Master[2](172.30.0.103:6379):负责槽 10923-16383(5461 个槽)

2. 副本分配(Replicas Setup)

每个主节点配置了 2 个从节点(副本):

  • 主节点 172.30.0.101 的从节点:

    • 172.30.0.105:6379

    • 172.30.0.106:6379

  • 主节点 172.30.0.102 的从节点:

    • 172.30.0.107:6379

    • 172.30.0.108:6379

  • 主节点 172.30.0.103 的从节点:

    • 172.30.0.109:6379

    • 172.30.0.104:6379

3. 节点角色列表

  • 主节点(Masters)

    • 172.30.0.101:6379 (ID: e01fd92...):槽 0-5460

    • 172.30.0.102:6379 (ID: dfa3629...):槽 5461-10922

    • 172.30.0.103:6379 (ID: 2ba7a32...):槽 10923-16383

  • 从节点(Replicas)

    • 172.30.0.104:6379 → 复制主节点 172.30.0.103

    • 172.30.0.105:6379 → 复制主节点 172.30.0.101

    • 172.30.0.106:6379 → 复制主节点 172.30.0.101

    • 172.30.0.107:6379 → 复制主节点 172.30.0.102

    • 172.30.0.108:6379 → 复制主节点 172.30.0.102

    • 172.30.0.109:6379 → 复制主节点 172.30.0.103


上面就是告诉我们它会构建什么样的集群,然后留下了下面这个

Can I set the above configuration? (type 'yes' to accept)

现在我们需要输入 yes 后,才会真正构建集群。

必须看到OK,才算是构建集群完毕了。

3.5.使用集群

我们现在来尝试登陆一下

其实在 Redis 集群环境中,客户端连接到集群中的任何一个节点,就等同于连接到了整个集群

我们可以执行下面这个命令来查看一下集群的信息

cluster nodes

我们看看

root@VM-16-14-ubuntu:~/redis-cluster# redis-cli -p 6379
127.0.0.1:6379> cluster nodes
f8f2688f7bead3d861538477d0d1e799fbe42a61 172.30.0.105:6379@16379 slave e01fd92d7cc7a84a1071738f39e3975a05399285 0 1755161671593 5 connected
939bdbb5de729e2cd4a7e8d1bee39edf9ba77e73 172.30.0.104:6379@16379 slave 2ba7a320903395b015a64f31d4cd7f36efe9365c 0 1755161671087 4 connected
283a5e96f3539496eb568ceaaae4c593fdefba6a 172.30.0.108:6379@16379 slave dfa3629b45fefec3ab6e02013499036114188e49 0 1755161671000 8 connected
e01fd92d7cc7a84a1071738f39e3975a05399285 172.30.0.101:6379@16379 master - 0 1755161671593 1 connected 0-5460
dfa3629b45fefec3ab6e02013499036114188e49 172.30.0.102:6379@16379 master - 0 1755161672094 2 connected 5461-10922
2ba7a320903395b015a64f31d4cd7f36efe9365c 172.30.0.103:6379@16379 master - 0 1755161671087 3 connected 10923-16383
574c53341a8ff72ceca4c39a4950a14b3ed40131 172.30.0.109:6379@16379 myself,slave 2ba7a320903395b015a64f31d4cd7f36efe9365c 0 1755161671000 9 connected
ad90ac3f6e05f7179076ee9db7f85fd4f5e60f00 172.30.0.106:6379@16379 slave e01fd92d7cc7a84a1071738f39e3975a05399285 0 1755161671593 6 connected
f6f6c095828fe1b1697dc7f76568c346f09c9e75 172.30.0.107:6379@16379 slave dfa3629b45fefec3ab6e02013499036114188e49 0 1755161671000 7 connected

接下来我们通过集群来存储数据

我们先登陆其中的一个主节点

redis-cli -h 172.30.0.101 -p 6379

我们发现报错了

错误信息 (error) MOVED 12706 172.30.0.103:6379 表示:key k1 属于槽位 12706,该槽位实际由节点 172.30.0.103:6379 负责处理,而当前节点是172.30.0.101:6379, 请求被当前节点拒绝。 

嗯?不对啊

我们曾在2.3.1.小节里面说过:

当客户端向 Redis 集群发送一个写命令(如 SET key value)时,集群需要确保该键值对被正确地存储到负责其哈希槽的节点上。这个过程涉及客户端和节点的协作:

  1. 客户端计算目标槽位:

    • 客户端(通常是支持集群模式的 Redis 客户端库)自己使用与集群相同的算法计算键 key 的哈希槽:slot = crc16(key) % 16384

    • 客户端需要知道集群的槽位分配信息(即哪个节点负责哪个槽位)。

  2. 客户端定位目标节点:

    • 客户端在本地缓存了一份 集群槽位映射表(Cluster Slot Map)。这份映射表记录了所有 16384 个槽位当前由哪个主节点负责(包含节点 ID、IP、端口等信息)。

    • 客户端根据计算出的 slot 号,查询本地缓存的槽位映射表,找到负责该槽位的目标节点(假设为 Node X)。

    • 关键点: 客户端需要具备集群感知能力,理解槽位映射。

  3. 客户端发送命令:

    • 客户端将写命令(SET key value直接发送给它在步骤 2 中定位到的目标节点 Node X

那这里怎么又会出现现在这种错误

事实上,我们在2.3.1.小节里面讲的情况都是客户端处于集群状态(带-c选项)下的情况。

客户端是分为集群感知模式和普通单机模式的

而我们现在是使用下面这个命令登陆的服务器

redis-cli -h 172.30.0.101 -p 6379

也就是处于普通单机模式!!而普通单机模式就是下面这样子

普通单机模式 (未加 -c 选项 / 非集群感知客户端):

  • 客户端没有集群感知能力,它仅仅把自己当作连接到了一个单一的 Redis 服务器 (172.30.0.101:6379)。

  • 不会主动获取或缓存集群的槽位映射信息。

  • 当它向当前连接的节点 (172.30.0.101:6379) 发送命令 SET k1 value 时:

    • 节点 172.30.0.101:6379 接收到命令。

    • 节点计算 k1 的哈希槽 (12706)。

    • 节点检查自身负责的槽位范围,发现槽 12706 并不归自己管理。

    • 因为客户端处于非集群模式,节点不会尝试代理或转发这个请求。

    • 节点直接向客户端返回一个 MOVED 错误,明确告知:(error) MOVED 12706 172.30.0.103:6379。这个错误信息的意思是:“你要操作的 Key k1 属于槽 12706,这个槽目前由节点 172.30.0.103:6379 负责,请直接联系它”。

如果说我们使用客户端登陆的时候,是带了-c选项的话,客户端就会处于集群感知模式

redis-cli -h 172.30.0.101 -p 6379 -c

那么情况就会变成下面这样:

集群感知模式 (redis-cli -c / 支持集群的客户端库):

  • 客户端启动时(或首次连接后)会主动获取并缓存整个集群的槽位分配信息(Slot Map),知道哪个节点管理哪些槽。

  • 当执行一个命令(如 SET k1 value)时,客户端自己先计算 k1 的哈希槽 (12706)。

  • 根据缓存的 Slot Map,客户端知道槽 12706 属于节点 172.30.0.103:6379

  • 客户端会直接将命令发送给正确的目标节点 172.30.0.103:6379 执行。

  • 这种模式下,客户端像一个智能路由器,对用户隐藏了集群的分布细节,提供了透明的访问体验。


redis的-c选项

为了确保客户端能够无缝、透明地操作分布在集群不同节点上的数据,一个至关重要的步骤是在启动 Redis 客户端命令时添加 -c 选项(即启用集群模式)。

非集群模式 (-c 未启用) 的局限性:

  • 当客户端以普通单机模式(未使用 -c 选项)连接到集群中的某个节点(例如 172.30.0.101:6379)时,它仅能访问恰好存储在该节点所负责的哈希槽(Slot)上的数据

  • 如果客户端尝试操作的 Key 经过集群哈希算法计算后,其所属的哈希槽并非由当前连接的节点管理,那么这次操作将不可避免地失败。此时,当前节点不会尝试处理或代理该请求,而是会直接向客户端返回一个 MOVED 错误(例如 (error) MOVED 12706 172.30.0.103:6379)。该错误清晰地指明了目标 Key 实际归属的槽位以及负责该槽位的正确节点地址。

集群模式 (-c 启用) 的智能处理:

  • 启用 -c 选项后,客户端便具备了集群感知能力。在建立连接后,客户端会主动获取并维护一份集群的槽位映射表(Slot Map),该表记录了每个哈希槽当前由哪个节点负责。

  • 当客户端执行一个操作命令(如 GET k1 或 SET k2 value)时:

    1. 客户端自身会计算目标 Key 的哈希槽号。

    2. 根据本地缓存的槽位映射表,客户端确定负责该槽位的目标节点

    3. 如果目标节点就是当前连接的节点,命令直接在该节点执行。

    4. 最关键的是:如果计算发现目标 Key 属于另一个节点管理的槽位,集群感知模式的客户端会自动处理重定向:

      • 它会透明地将请求路由(转发) 到正确的目标节点执行。

      • 这个过程对用户(或应用程序)是完全透明的,用户无需关心 Key 的实际物理位置。

      • 操作结果会从目标节点经由客户端返回给用户,就像操作发生在最初连接的节点一样。

  • 因此,启用 -c 选项后,客户端就像一个智能代理,自动处理了跨节点访问的复杂性,使得整个集群在逻辑上如同一个单一的数据存储。

设计价值与用户体验:

这种基于 -c 选项的集群感知设计带来了显著的易用性优势

  • 简化访问: 用户或应用程序无需预先知晓或硬编码每个 Key 的具体存储位置

  • 统一入口: 可以通过任意一个集群节点作为入口来访问整个集群中的所有数据。

  • 位置透明: Key 在集群节点间的分布细节被完全隐藏,用户操作集群数据与操作单机 Redis 实例的体验高度一致

  • 提升效率: 客户端自动维护槽位映射并在必要时重定向请求,避免了手动处理 MOVED 错误的繁琐,提高了开发和运维效率。


我们看例子

    我们发现我们的客户端把这个请求转发给了103

    我们多看几个例子

    注意:

    • 使用集群之后, 之前学过的命令, 都能正常使用. (不够严谨)  
    • 有些指令, 是能够操作多个 key 的.  
    • 此时如果 key 是分散在不同的分片上, 就可能出现问题了.

     

    我们看看在从节点上面执行写操作会怎么样

    我们上面是在一个主节点上面操作的,那现在从节点的情况我们也要看看

    我们发现从节点也是把这个写操作转发到对应的主节点上面去。我们再看看

    3.6.故障转移

    3.6.1.主节点宕机演示

    我们手动停止一个主节点

    我们要停止172.30.0.101:6379这个主节点,那也就是redis1

    docker stop redis1

     运行结果

    root@VM-16-14-ubuntu:~/redis-cluster# docker ps
    CONTAINER ID   IMAGE         COMMAND                  CREATED       STATUS       PORTS                                                                                          NAMES
    4085f8d9c09e   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6380->6379/tcp, [::]:6380->6379/tcp, 0.0.0.0:16380->16379/tcp, [::]:16380->16379/tcp   redis10
    c87a36c190ff   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6377->6379/tcp, [::]:6377->6379/tcp, 0.0.0.0:16377->16379/tcp, [::]:16377->16379/tcp   redis7
    748d9dcf4f18   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6375->6379/tcp, [::]:6375->6379/tcp, 0.0.0.0:16375->16379/tcp, [::]:16375->16379/tcp   redis5
    f1e68d25f634   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6376->6379/tcp, [::]:6376->6379/tcp, 0.0.0.0:16376->16379/tcp, [::]:16376->16379/tcp   redis6
    213e6228fa4a   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6371->6379/tcp, [::]:6371->6379/tcp, 0.0.0.0:16371->16379/tcp, [::]:16371->16379/tcp   redis1
    c96215c7c861   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp, 0.0.0.0:16379->16379/tcp, [::]:16379->16379/tcp   redis9
    ece122b66418   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6373->6379/tcp, [::]:6373->6379/tcp, 0.0.0.0:16373->16379/tcp, [::]:16373->16379/tcp   redis3
    8929e31cee94   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6372->6379/tcp, [::]:6372->6379/tcp, 0.0.0.0:16372->16379/tcp, [::]:16372->16379/tcp   redis2
    7a7db20f75ef   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6381->6379/tcp, [::]:6381->6379/tcp, 0.0.0.0:16381->16379/tcp, [::]:16381->16379/tcp   redis11
    d5271e1c3347   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6378->6379/tcp, [::]:6378->6379/tcp, 0.0.0.0:16378->16379/tcp, [::]:16378->16379/tcp   redis8
    e7c552f8dd72   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6374->6379/tcp, [::]:6374->6379/tcp, 0.0.0.0:16374->16379/tcp, [::]:16374->16379/tcp   redis4
    root@VM-16-14-ubuntu:~/redis-cluster# docker stop redis1
    redis1
    root@VM-16-14-ubuntu:~/redis-cluster# docker ps
    CONTAINER ID   IMAGE         COMMAND                  CREATED       STATUS       PORTS                                                                                          NAMES
    4085f8d9c09e   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6380->6379/tcp, [::]:6380->6379/tcp, 0.0.0.0:16380->16379/tcp, [::]:16380->16379/tcp   redis10
    c87a36c190ff   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6377->6379/tcp, [::]:6377->6379/tcp, 0.0.0.0:16377->16379/tcp, [::]:16377->16379/tcp   redis7
    748d9dcf4f18   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6375->6379/tcp, [::]:6375->6379/tcp, 0.0.0.0:16375->16379/tcp, [::]:16375->16379/tcp   redis5
    f1e68d25f634   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6376->6379/tcp, [::]:6376->6379/tcp, 0.0.0.0:16376->16379/tcp, [::]:16376->16379/tcp   redis6
    c96215c7c861   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp, 0.0.0.0:16379->16379/tcp, [::]:16379->16379/tcp   redis9
    ece122b66418   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6373->6379/tcp, [::]:6373->6379/tcp, 0.0.0.0:16373->16379/tcp, [::]:16373->16379/tcp   redis3
    8929e31cee94   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6372->6379/tcp, [::]:6372->6379/tcp, 0.0.0.0:16372->16379/tcp, [::]:16372->16379/tcp   redis2
    7a7db20f75ef   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6381->6379/tcp, [::]:6381->6379/tcp, 0.0.0.0:16381->16379/tcp, [::]:16381->16379/tcp   redis11
    d5271e1c3347   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6378->6379/tcp, [::]:6378->6379/tcp, 0.0.0.0:16378->16379/tcp, [::]:16378->16379/tcp   redis8
    e7c552f8dd72   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6374->6379/tcp, [::]:6374->6379/tcp, 0.0.0.0:16374->16379/tcp, [::]:16374->16379/tcp   redis4
    

    重新随机连接一个节点看看

    我们发现主节点个数还是3个啊!!

    现在我们重新启动redis1看看

    docker start redis1

    运行结果

    root@VM-16-14-ubuntu:~/redis-cluster# docker ps
    CONTAINER ID   IMAGE         COMMAND                  CREATED       STATUS       PORTS                                                                                          NAMES
    4085f8d9c09e   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6380->6379/tcp, [::]:6380->6379/tcp, 0.0.0.0:16380->16379/tcp, [::]:16380->16379/tcp   redis10
    c87a36c190ff   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6377->6379/tcp, [::]:6377->6379/tcp, 0.0.0.0:16377->16379/tcp, [::]:16377->16379/tcp   redis7
    748d9dcf4f18   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6375->6379/tcp, [::]:6375->6379/tcp, 0.0.0.0:16375->16379/tcp, [::]:16375->16379/tcp   redis5
    f1e68d25f634   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6376->6379/tcp, [::]:6376->6379/tcp, 0.0.0.0:16376->16379/tcp, [::]:16376->16379/tcp   redis6
    c96215c7c861   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp, 0.0.0.0:16379->16379/tcp, [::]:16379->16379/tcp   redis9
    ece122b66418   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6373->6379/tcp, [::]:6373->6379/tcp, 0.0.0.0:16373->16379/tcp, [::]:16373->16379/tcp   redis3
    8929e31cee94   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6372->6379/tcp, [::]:6372->6379/tcp, 0.0.0.0:16372->16379/tcp, [::]:16372->16379/tcp   redis2
    7a7db20f75ef   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6381->6379/tcp, [::]:6381->6379/tcp, 0.0.0.0:16381->16379/tcp, [::]:16381->16379/tcp   redis11
    d5271e1c3347   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6378->6379/tcp, [::]:6378->6379/tcp, 0.0.0.0:16378->16379/tcp, [::]:16378->16379/tcp   redis8
    e7c552f8dd72   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours   0.0.0.0:6374->6379/tcp, [::]:6374->6379/tcp, 0.0.0.0:16374->16379/tcp, [::]:16374->16379/tcp   redis4
    root@VM-16-14-ubuntu:~/redis-cluster# docker start redis1
    redis1
    root@VM-16-14-ubuntu:~/redis-cluster# docker ps
    CONTAINER ID   IMAGE         COMMAND                  CREATED       STATUS        PORTS                                                                                          NAMES
    4085f8d9c09e   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours    0.0.0.0:6380->6379/tcp, [::]:6380->6379/tcp, 0.0.0.0:16380->16379/tcp, [::]:16380->16379/tcp   redis10
    c87a36c190ff   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours    0.0.0.0:6377->6379/tcp, [::]:6377->6379/tcp, 0.0.0.0:16377->16379/tcp, [::]:16377->16379/tcp   redis7
    748d9dcf4f18   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours    0.0.0.0:6375->6379/tcp, [::]:6375->6379/tcp, 0.0.0.0:16375->16379/tcp, [::]:16375->16379/tcp   redis5
    f1e68d25f634   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours    0.0.0.0:6376->6379/tcp, [::]:6376->6379/tcp, 0.0.0.0:16376->16379/tcp, [::]:16376->16379/tcp   redis6
    213e6228fa4a   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 1 second   0.0.0.0:6371->6379/tcp, [::]:6371->6379/tcp, 0.0.0.0:16371->16379/tcp, [::]:16371->16379/tcp   redis1
    c96215c7c861   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours    0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp, 0.0.0.0:16379->16379/tcp, [::]:16379->16379/tcp   redis9
    ece122b66418   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours    0.0.0.0:6373->6379/tcp, [::]:6373->6379/tcp, 0.0.0.0:16373->16379/tcp, [::]:16373->16379/tcp   redis3
    8929e31cee94   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours    0.0.0.0:6372->6379/tcp, [::]:6372->6379/tcp, 0.0.0.0:16372->16379/tcp, [::]:16372->16379/tcp   redis2
    7a7db20f75ef   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours    0.0.0.0:6381->6379/tcp, [::]:6381->6379/tcp, 0.0.0.0:16381->16379/tcp, [::]:16381->16379/tcp   redis11
    d5271e1c3347   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours    0.0.0.0:6378->6379/tcp, [::]:6378->6379/tcp, 0.0.0.0:16378->16379/tcp, [::]:16378->16379/tcp   redis8
    e7c552f8dd72   redis:5.0.9   "docker-entrypoint.s…"   5 hours ago   Up 5 hours    0.0.0.0:6374->6379/tcp, [::]:6374->6379/tcp, 0.0.0.0:16374->16379/tcp, [::]:16374->16379/tcp   redis4
    

    现在我们回去看看,也是随机连接一个节点

    有人就说了,这和哨兵不是有点像吗?但事实上,还是有区别的。

    3.6.2.故障转移的过程

    Redis集群故障处理流程

    1) 故障判定:从心跳到客观下线

    Redis集群中的节点通过周期性的心跳包维持通信和状态感知:

    1. 心跳机制: 节点A向节点B发送一个PING包,节点B收到后必须回复一个PONG包。除了message type属性不同外,PINGPONG包的内容完全相同,都包含了发送节点的关键集群配置信息(节点ID、所属分片、角色-主/从、主节点ID、持有的slots位图等)。

    2. 随机通信策略: 为了有效控制网络流量,尤其是在大型集群中(节点数N会导致全连接心跳数呈N²级增长),每个节点每秒只会随机选择一部分其他节点发送PING包,而不是向所有节点发送。

    3. 主观下线 (PFAIL): 如果节点A向节点B发送PING后,在预期时间内未收到PONG回复,节点A会尝试重新建立与B的TCP连接。若重连也失败,节点A则会将节点B标记为PFAIL(主观下线)状态。这表明在节点A的视角里,节点B可能出现了故障。

    4. 状态传播与确认: 节点A判定B为PFAIL后,会利用Redis内置的Gossip协议将这一信息传播给集群中的其他节点。每个节点都维护着自己的“下线列表”(可能因视角不同而各异)。节点A会向其他节点查询它们对节点B状态的看法。

    5. 客观下线 (FAIL): 当节点A收集到的信息表明,认为节点B处于PFAIL状态的节点数量超过了集群所有主节点总数的一半时,节点A会将节点B的状态升级为FAIL(客观下线)。节点A随后会向整个集群广播一条FAIL消息。收到此消息的其他节点也会将节点B标记为FAIL

    至此,节点B被整个集群确认为故障节点。

    2) 故障迁移:从节点晋升

    当故障节点B被标记为FAIL且其身份是主节点时,其从节点(例如C和D)将触发故障迁移过程,目标是将一个从节点提升(晋升)为新的主节点,继续负责原主节点持有的slots,恢复集群的完整服务能力。

    故障迁移的具体流程如下:

    1. 资格检查: 每个从节点(C和D)首先检查自身是否具备参与选举的资格。判断标准是与故障主节点B的最后一次成功通信时间。如果通信中断时间超过预设阈值cluster-node-timeout * cluster-replica-validity-factor),则认为该从节点的数据可能过于陈旧(与主节点差异太大),丧失竞选资格。

    2. 延迟竞选: 具备资格的从节点不会立即开始竞选,而是会进入一个延迟等待期。其计算公式为:延迟时间 = 500ms (基础时间) + [0, 500ms] (随机时间) + 排名 * 1000ms。这里的“排名”基于从节点的复制偏移量(replication offset)。复制偏移量越大(数据越新)的从节点,排名值越小,计算出的延迟时间也就越短。这个设计旨在让数据最新的从节点有更高的概率率先唤醒。

    3. 发起拉票: 当某个从节点(例如C)的延迟时间结束醒来后,它会立即向集群中所有主节点发送拉票请求(FAILOVER_AUTH_REQUEST)。注意,只有主节点才拥有投票权。

    4. 投票与晋升: 收到拉票请求的主节点,如果在该轮选举中尚未投票,并且认可C的资格(通常都会认可),则会将自己的唯一一票投给C。一旦C收集到的票数超过集群中所有主节点总数的一半,C就赢得了选举。获胜后,C会立即执行SLAVEOF NO ONE命令将自己提升为主节点,并通知其他从节点(如D)执行SLAVEOF命令指向新的主节点C(即让D成为C的从节点)。

    5. 状态同步: 新主节点C会向整个集群广播PONG消息,其中包含它已晋升为主节点并接管了原主节点B的slots的信息。集群中的所有其他节点收到此消息后,都会更新本地维护的集群配置信息,记录C为新的主节点。

    🥇核心算法: 上述选举过程本质上是Raft共识算法的一个精简实现,是分布式系统中广泛用于领导者选举的核心算法。通过引入随机延迟和基于数据新鲜度的优先级(偏移量),该机制能在大多数情况下高效、快速地选出一个新的主节点,且数据最新的从节点具有较高的选举成功率

    集群整体失效 (FAIL) 的条件:

    在Redis集群中,核心原则是必须确保每个哈希槽(slot)都能被正常访问,以维持整个集群的可用性和数据完整性。虽然集群设计具有高可用性,能够容忍部分节点故障并通过故障转移机制恢复,但在某些严重的故障场景下,整个集群会进入FAIL状态。此状态意味着集群停止提供写入服务(可能也停止读取服务),本质上处于不可用状态。

    触发集群整体失效 (FAIL) 的具体条件如下:

    1. 分片完全宕机:

      • 场景描述: 集群中某个负责特定哈希槽范围的分片(即一个主节点及其所有从节点组成的复制组)完全崩溃

      • 触发原因: 该分片的主节点和所有从节点同时故障或不可达

      • 后果: 该分片负责的所有哈希槽彻底失去服务能力,无法进行任何读写操作。由于集群无法保证所有槽位可用,为维护数据一致性,整个集群进入FAIL状态。

    2. 主节点孤立且无替补:

      • 场景描述: 某个分片的主节点发生故障,但该分片下没有可用的从节点

      • 触发原因: 主节点宕机或网络隔离,且该分片不存在从节点,或者存在的从节点因数据过旧、网络问题或本身故障等原因无法晋升为新主节点。

      • 后果: 该主节点负责的哈希槽立即失去写入能力(可能也失去读取能力)。由于没有可用的从节点进行故障转移来接管这些槽位,集群无法恢复该分片的服务,导致整体失效。

    3. 主节点多数失效:

      • 场景描述: 集群中超过半数(N/2 + 1)的主节点同时发生故障或不可达

      • 触发原因: 可能由于大规模硬件故障、网络分区(裂脑)或灾难性事件导致多个主节点同时下线。

      • 后果: 集群丧失了达成共识(Quorum)的能力。故障转移、槽位分配、配置变更等关键决策都需要多数主节点的投票同意才能进行。超过半数主节点失效使得集群无法形成有效的多数派来做出任何决策(包括判定其他节点状态或执行故障转移),整个集群因此陷入不可用状态。

    总结: 以上三种情况的共同本质在于集群无法满足“每个槽位都有可用节点提供服务”这一最低要求,或者丧失了维持自身状态和进行决策所需的基本共识机制。一旦检测到这些情况,Redis集群会主动将自身状态置为FAIL,以阻止可能导致数据不一致或错误操作的客户端请求,确保数据安全,并明确告知管理员集群处于需要人工干预的严重故障状态。

    3.6.3.查看日志

    我们可以回到刚刚那个目录,执行下面这个命令查看日志

    docker-compose ps

    1) 故障发生:主节点redis1主动关闭(13:16:32)

    redis1     | 1:signal-handler (1755177392) Received SIGTERM scheduling shutdown...
    # 信号处理线程收到SIGTERM终止信号,计划执行关闭流程
    redis1     | 1:M 14 Aug 2025 13:16:32.271 # User requested shutdown...
    # 主节点(M)确认用户请求关闭(SIGTERM信号触发)
    redis1     | 1:M 14 Aug 2025 13:16:32.271 * Calling fsync() on the AOF file.
    # 执行AOF文件同步(fsync),确保数据持久化到磁盘
    redis1     | 1:M 14 Aug 2025 13:16:32.272 # Redis is now ready to exit, bye bye...
    # 完成关闭准备,进程即将退出

    • 分析redis1(主节点)收到SIGTERM信号,主动关闭。这是人为触发的故障(例如运维操作)。redis1正常退出,但集群其他节点尚未感知,故障判定需依赖心跳超时。

    2) 从节点检测连接丢失:主观下线(PFAIL)开始(13:16:32)

    redis5     | 1:S 14 Aug 2025 13:16:32.274 # Connection with master lost.
    # 从节点(S) redis5 检测到与主节点(redis1)的连接断开
    redis5     | 1:S 14 Aug 2025 13:16:32.274 * Caching the disconnected master state.
    # 缓存主节点断开前的状态信息(用于后续故障转移)
    redis6     | 1:S 14 Aug 2025 13:16:32.273 # Connection with master lost.
    # 从节点(S) redis6 同样检测到主节点连接断开
    redis6     | 1:S 14 Aug 2025 13:16:32.273 * Caching the disconnected master state.
    # 缓存主节点状态

    分析

    • redis5redis6redis1的从节点(日志中角色标记为S表示slave)。

    • 它们几乎同时检测到与主节点redis1的连接丢失(时间差1ms),这对应于主观下线(PFAIL) 状态。

    • 从节点会缓存主节点状态,并开始通过Gossip协议向其他节点传播PFAIL信息(日志未直接显示传播细节,但后续客观下线证明其发生)。

    • 在 Redis 集群中,节点间通过 Gossip 协议传播 PFAIL(主观下线)状态的过程不会直接记录在日志中。Redis 的日志主要记录关键状态变更(如 FAIL 客观下线),而 Gossip 通信的细节默认不输出。

    3) 集群达成共识:标记redis1为客观下线(FAIL)(13:16:37 - 13:16:39)

    redis4     | 1:S 14 Aug 2025 13:16:37.574 * Marking node e01fd92... as failing (quorum reached).
    # 从节点 redis4 根据Gossip协议达成共识(quorum),标记节点e01fd92(redis1)为故障状态
    redis4     | 1:S 14 Aug 2025 13:16:37.574 # Cluster state changed: fail
    # 集群状态变更为"fail"(部分节点不可用)
    
    redis8     | 1:S 14 Aug 2025 13:16:37.595 * Marking node e01fd92... as failing (quorum reached).
    redis8     | 1:S 14 Aug 2025 13:16:37.596 # Cluster state changed: fail
    # redis8 同样标记主节点故障(日志含义同上)
    
    redis9     | 1:S 14 Aug 2025 13:16:38.575 * Marking node e01fd92... as failing (quorum reached).
    redis9     | 1:S 14 Aug 2025 13:16:38.575 # Cluster state changed: fail
    # redis9 确认故障(Gossip传播延迟导致稍晚记录)
    
    redis7     | 1:S 14 Aug 2025 13:16:38.875 * Marking node e01fd92... as failing (quorum reached).
    redis7     | 1:S 14 Aug 2025 13:16:38.875 # Cluster state changed: fail
    # redis7 确认故障
    
    redis6     | 1:S 14 Aug 2025 13:16:38.979 * Marking node e01fd92... as failing (quorum reached).
    redis6     | 1:S 14 Aug 2025 13:16:38.979 # Cluster state changed: fail
    # redis6(原主节点的从节点)确认主节点故障
    
    redis3     | 1:M 14 Aug 2025 13:16:39.075 * Marking node e01fd92... as failing (quorum reached).
    redis3     | 1:M 14 Aug 2025 13:16:39.076 # Cluster state changed: fail
    # 主节点(M) redis3 确认故障
    
    redis2     | 1:M 14 Aug 2025 13:16:39.076 * Marking node e01fd92... as failing (quorum reached).
    redis2     | 1:M 14 Aug 2025 13:16:39.076 # Cluster state changed: fail
    # 主节点(M) redis2 最终确认故障(集群多数节点达成共识)

    关键日志:多个节点(redis4redis8redis9redis7redis6redis3redis2)在约5秒内(13:16:32 → 13:16:37)标记节点e01fd92d...(即redis1)为failing

    分析

    • 客观下线(FAIL)达成:节点通过Gossip协议交换状态,当认为redis1处于PFAIL状态的主节点数超过集群主节点总数的一半时,redis1被标记为FAIL(日志中quorum reached表明共识达成)。这是故障处理流程中的关键步骤。

    • 时间延迟:从连接丢失(13:16:32)到客观下线(13:16:37)约5秒,这由cluster-node-timeout参数决定(默认15秒,但此处较短,可能是自定义配置)。

    • 集群状态临时失效:各节点日志显示Cluster state changed: fail,表示集群因主节点故障暂时不可用(但未整体失效,因为从节点可用)。

    • 节点角色redis2redis3标记为M(master),表明它们是其他分片的主节点,拥有投票权。

    4) 故障迁移选举启动:从节点计算延迟(13:16:39)

    redis5     | 1:S 14 Aug 2025 13:16:39.077 # Start of election delayed for 760 milliseconds (rank #0, offset 22665).
    # redis5 延迟760ms启动选举(rank#0表示优先级最高,22665为复制偏移量)
    
    redis6     | 1:S 14 Aug 2025 13:16:39.079 # Start of election delayed for 763 milliseconds (rank #0, offset 22665).
    # redis6 延迟763ms启动选举(相同偏移量但延迟更长,将失去先机)

    分析

    • 资格检查通过redis5redis6均参与选举,表明它们与redis1的最后通信时间未超阈值(数据未过旧)。

    • 延迟计算

      • 公式:延迟时间 = 500ms + [0,500ms]随机时间 + 排名 * 1000ms

      • 两者offset均为22665,复制偏移量相同,表明数据同步状态一致,因此排名均为#0(数据最新)。

      • 延迟差异

        • redis5760ms = 500ms + 260ms(随机) + 0 * 1000ms

        • redis6763ms = 500ms + 263ms(随机) + 0 * 1000ms

      • 随机时间差异(260ms vs 263ms)确保数据相同的节点不会同时唤醒,避免选举冲突。redis5延迟更短,因此更早唤醒。

    • Raft算法体现:优先级机制(数据新鲜度)和随机延迟确保在多数情况下数据最新的节点优先赢得选举。

    5) redis5赢得选举并晋升为新主节点(13:16:39)

    redis5     | 1:S 14 Aug 2025 13:16:39.878 # Starting a failover election for epoch 10.
    # 在配置纪元(epoch) 10 启动故障转移选举
    redis5     | 1:S 14 Aug 2025 13:16:39.886 # Failover election won: I'm the new master.
    # 选举获胜,晋升为主节点
    redis5     | 1:S 14 Aug 2025 13:16:39.886 # configEpoch set to 10 after successful failover
    # 成功故障转移后更新配置纪元为10(集群唯一事务ID)
    redis5     | 1:M 14 Aug 2025 13:16:39.886 # Cluster state changed: ok
    # 节点角色变为主节点(M),集群状态恢复OK

    分析

    • 选举启动redis5在延迟结束(13:16:39.077 + 760ms ≈ 13:16:39.837)后,于13:16:39.878开始选举(Starting a failover election for epoch 10)。epoch 10表示选举轮次。

    • 快速胜出:仅8ms后(13:16:39.886),redis5宣布赢得选举(Failover election won)。速度极快,表明它迅速获得多数主节点投票。

    • 角色变更redis5S(slave)晋升为M(master),configEpoch更新为10(集群配置版本号递增),并接管redis1的slots。

    • 集群状态恢复redis5的集群状态立即变为ok,表示其分片已恢复服务。

    6) 其他主节点投票确认(13:16:39)

    redis2     | 1:M 14 Aug 2025 13:16:39.882 # Failover auth granted to f8f2688... for epoch 10
    # 主节点 redis2 为 redis5(节点ID f8f2688)投票
    
    redis2     | 1:M 14 Aug 2025 13:16:39.886 # Failover auth denied to ad90ac3...: already voted for epoch 10
    # 拒绝 redis6(节点ID ad90ac3)的投票请求(同一纪元只能投一票)
    
    redis3     | 1:M 14 Aug 2025 13:16:39.882 # Failover auth granted to f8f2688... for epoch 10
    redis3     | 1:M 14 Aug 2025 13:16:39.885 # Failover auth denied to ad90ac3...: already voted for epoch 10
    # redis3 同样投票给 redis5 并拒绝 redis6

    分析

    • 投票逻辑

      • f8f2688f...redis5的节点ID,ad90ac3f...redis6的节点ID。

      • redis2redis3(主节点)在13:16:39.882授予redis5投票权限(Failover auth granted),表明它们收到FAILOVER_AUTH_REQUEST并投票。

      • 多数票达成:假设集群有N个主节点(日志中至少redis2redis3等),redis5快速获得多数票(超过N/2)。

    • 拒绝redis6的请求:稍后redis6尝试拉票时(13:16:39.881开始选举),主节点拒绝(Failover auth denied),因为已为epoch 10投票给redis5。这符合“每轮选举仅一票”的规则。

    • Raft算法体现:投票机制确保只有一个节点胜出,避免脑裂。

    7) redis6自动切换为redis5的从节点(13:16:39)

    redis6     | 1:S 14 Aug 2025 13:16:39.881 # Starting a failover election for epoch 10.
    # redis6 尝试启动选举(但晚于redis5)
    
    redis6     | 1:S 14 Aug 2025 13:16:39.892 # Configuration change detected. Reconfiguring myself as a replica of f8f2688...
    # 检测到集群配置变更(redis5已胜选),自动将自己重配置为 redis5 的从节点
    
    redis6     | 1:S 14 Aug 2025 13:16:39.892 # Cluster state changed: ok
    # 集群状态恢复OK

    分析

    • 选举启动延迟redis6在延迟结束后(13:16:39.079 + 763ms ≈ 13:16:39.842)于13:16:39.881开始选举。

    • 配置更新检测:但仅11ms后(13:16:39.892),redis6检测到集群配置已变更(Configuration change detected),即redis5已晋升为主节点。

    • 自动降级为从节点redis6放弃选举,将自己重新配置为redis5的从节点(replica of f8f268...)。这是故障迁移的正常行为:当新主节点已选出时,其他从节点自动同步。

    • 无冲突redis6未实际拉票即退出选举,避免无效竞争。

    8) 集群状态全面恢复(13:16:39)

    redis3     | 1:M 14 Aug 2025 13:16:39.891 # Cluster state changed: ok
    redis2     | 1:M 14 Aug 2025 13:16:39.890 # Cluster state changed: ok
    redis4     | 1:S 14 Aug 2025 13:16:39.889 # Cluster state changed: ok
    redis7     | 1:S 14 Aug 2025 13:16:39.890 # Cluster state changed: ok
    redis8     | 1:S 14 Aug 2025 13:16:39.889 # Cluster state changed: ok
    redis9     | 1:S 14 Aug 2025 13:16:39.889 # Cluster state changed: ok
    # 所有节点(主节点M/从节点S)确认集群状态恢复OK(故障转移完成)

    分析

    • 配置广播与同步:新主节点redis5广播PONG消息(日志未直接显示,但隐含在状态变更中),其他节点更新集群配置。

    • 全局状态恢复:所有节点(主节点如redis2redis3,从节点如redis4redis7等)在13:16:39.889-39.891报告Cluster state changed: ok,表明集群已完全恢复,所有slots可访问。

    • 耗时总计:从故障发生(13:16:32)到集群恢复(13:16:39)约7秒,符合Redis集群高效故障转移的设计。

    9. redis1 重启后重新加入集群(13:21:38)

    redis1     | 1:C 14 Aug 2025 13:21:38.757 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
    Redis1 启动进程开始初始化(C 表示子进程/启动进程)。
    
    redis1     | 1:M 14 Aug 2025 13:21:38.760 * Node configuration loaded, I'm e01fd92d7cc7a84a1071738f39e3975a05399285
    Redis1 主进程(M)成功加载集群配置,并声明自身节点 ID e01fd92...。
    
    redis1     | 1:M 14 Aug 2025 13:21:38.765 # Configuration change detected. Reconfiguring myself as a replica of f8f2688f7bead3d861538477d0d1e799fbe42a61
    Redis1 检测到配置变更,将自己重新配置为节点 f8f2688... 的从节点(副本)。
    
    redis1     | 1:S 14 Aug 2025 13:21:38.765 # Cluster state changed: ok
    Redis1 角色已切换为从节点(S),集群状态恢复为 OK。

    10. 集群清除 redis1 的 FAIL 状态(13:21:38)

    所有节点检测到 redis1 恢复:

    redis5     | 1:M 14 Aug 2025 13:21:38.783 * Clear FAIL state for node e01fd92d7cc7a84a1071738f39e3975a05399285: master without slots is reachable again.
    Redis5(主节点)清除 Redis1 的 FAIL 状态,因为无槽的主节点 Redis1 已恢复可达。
    
    redis6     | 1:S 14 Aug 2025 13:21:38.793 * Clear FAIL state for node e01fd92d7cc7a84a1071738f39e3975a05399285: master without slots is reachable again.
    Redis6(从节点)清除 Redis1 的 FAIL 状态,确认其恢复可达。
    
    redis7     | 1:S 14 Aug 2025 13:21:38.769 * Clear FAIL state for node e01fd92d7cc7a84a1071738f39e3975a05399285: master without slots is reachable again.
    Redis7(从节点)清除 Redis1 的 FAIL 状态,标记其恢复可用。
    
    redis9     | 1:S 14 Aug 2025 13:21:38.769 * Clear FAIL state for node e01fd92d7cc7a84a1071738f39e3975a05399285: master without slots is reachable again.
    Redis9(从节点)清除 Redis1 的 FAIL 状态,确认其重新加入集群。
    
    redis8     | 1:S 14 Aug 2025 13:21:38.771 * Clear FAIL state for node e01fd92d7cc7a84a1071738f39e3975a05399285: master without slots is reachable again.
    Redis8(从节点)清除 Redis1 的 FAIL 状态,声明其已恢复网络可达。
    
    redis4     | 1:S 14 Aug 2025 13:21:38.844 * Clear FAIL state for node e01fd92d7cc7a84a1071738f39e3975a05399285: master without slots is reachable again.
    Redis4(从节点)清除 Redis1 的 FAIL 状态,标识无槽主节点恢复可用。
    
    redis2     | 1:M 14 Aug 2025 13:21:38.840 * Clear FAIL state for node e01fd92d7cc7a84a1071738f39e3975a05399285: master without slots is reachable again.
    Redis2(主节点)清除 Redis1 的 FAIL 状态,确认其恢复服务。
    
    redis3     | 1:M 14 Aug 2025 13:21:38.842 * Clear FAIL state for node e01fd92d7cc7a84a1071738f39e3975a05399285: master without slots is reachable again.
    Redis3(主节点)清除 Redis1 的 FAIL 状态,完成集群故障恢复流程。

    3.7.集群扩容

    扩容是一个在开发中比较常遇到的场景.

    随着业务的发展, 现有集群很可能无法容纳日益增长的数据. 此时给集群中加入更多新的机器, 就可以使存储的空间更大了.

    所谓分布式的本质, 就是使用更多的机器, 引入更多的硬件资源.

    3.7.1.第一步: 把新的主节点加入到集群

    上面已经把 redis1 - redis9 重新构成了集群. 接下来把 redis10 和 redis11 也加入集群.

    此处我们把 redis10 作为主机, redis11 作为从机.

    redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379

    部分说明
    redis-cli --cluster add-nodeRedis 集群管理命令,表示执行添加节点操作
    172.30.0.110:6379新节点的 IP 地址和端口(待加入集群的节点)
    172.30.0.101:6379现有集群中任意节点的 IP 地址和端口(作为接入点)
    • 节点 172.30.0.110:6379 会被添加到目标集群中

    • 新节点默认以 Master 角色(空节点) 加入,不持有任何 Slot

    看到OK即可,我们现在连接上这个节点看看

    我们发现这个刚添加进去的主节点没有槽位。

    3.7.2.第⼆步:重新分配slots

    redis-cli --cluster reshard 172.30.0.101:6379

    让我们分解一下它的含义:

    1. redis-cli: Redis 的命令行界面工具。

    2. --cluster: 这个选项告诉 redis-cli 你要执行的是与 Redis 集群管理相关的操作,而不是普通的单节点操作。

    3. reshard: 这是具体的集群操作子命令。它的核心功能是:

      • 迁移哈希槽: 从一个或多个现有的主节点(源节点)上移动指定数量的哈希槽(hash slots)到另一个主节点(目标节点)。

      • 数据迁移: 在移动槽位的过程中,属于这些槽位的键值对数据也会被自动地从源节点迁移到目标节点。

      • 集群配置更新: 迁移完成后,集群配置会自动更新,所有节点都会知道这些槽位的新归属(目标节点)。

    4. 172.30.0.101:6379: 这是集群中任何一个已知节点的 IP 地址和端口号redis-cli 会通过这个节点连接到集群,并发现集群中的所有其他节点。它不一定是源节点或目标节点,只是一个入口点。

    另外, 注意单词拼写, 是 reshard (重新切分), 不是 reshared (重新分享), 不要多写一个 e.

      执行之后, 会进入交互式操作, redis 会提示用户输入以下内容:

      • 多少个 slots 要进行 reshard ? (此处我们填写 4096)
      • 哪个节点来接收这些 slots ? (此处我们填写 172.30.0.110 这个节点的集群节点 id)
      • 这些 slots 从哪些节点搬运过来? (此处我们填写 all, 表示从其他所有的节点都进行搬运)

      我们按下回车

      输入yes即可

      现在搬运真正开始,不仅仅是槽重新划分,数据也会搬运到对应的主机上面。

      弄完之后我们随机登陆到集群的一个主机,查看一下

      现在4个主节点都有槽了。

      如果在搬运 slots / key 的过程中, 此时客户端能否访问咱们 redis 集群呢?

      在 Redis 集群进行数据迁移(即移动哈希槽 Slot 或键 Key)的过程中,客户端的访问能力取决于所访问 Key 的状态:

      1. 未迁移的 Key: 对于绝大部分未涉及当前迁移操作的 Key,客户端可以正常进行读写访问,不受迁移过程的影响。

      2. 正在迁移的 Key: 对于正处于迁移过程中的 Key,客户端的访问存在失败的可能性

      访问失败的具体场景解释:

      假设客户端尝试访问 Key k1。Redis 集群基于哈希槽的分片机制,通过计算 k1 的哈希值确定它属于 Slot X(例如第一个分片)。客户端会收到重定向(MOVED 或 ASK 指令),引导其去负责 Slot X 的当前源节点进行访问。

      然而,如果在客户端收到重定向指令后、实际向源节点发起请求前的极短时间窗口内,k1 恰好被完全迁移出了该源节点(即迁移完成,数据已转移至目标节点且 Slot 所有权已变更),那么当客户端的请求最终到达这个已不再负责 k1 或 Slot X 的源节点时,该节点将无法处理请求,从而导致客户端访问出错(例如收到 MOVED 错误指向新节点,或者连接超时等)。

      3.7.3.第三步:给新的主节点添加从节点

      光有主节点了,此时扩容的⽬标已经初步达成.但是为了保证集群可⽤性,还需要给这个新的主节点添加 从节点,保证该主节点宕机之后,有从节点能够顶上.

      redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave --cluster-master-id [172.30.1.110 节点的 nodeId]

      命令参数解析

      • redis-cli:Redis 客户端工具。
      • --cluster:启用集群模式(支持集群相关操作)。
      • add-node:子命令,表示“添加新节点”。
      • 172.30.0.111:6379新节点的地址和端口(即将加入集群的节点)。
      • 172.30.0.101:6379集群中任意一个现有节点的地址和端口(用于连接集群)。
      • --cluster-slave:指定新节点作为 从节点(Replica)。
      • --cluster-master-id [172.30.1.110 节点的 nodeId]:指定新从节点的 主节点 ID(即该从节点要复制的主节点的唯一标识)。

      有人就要问了,72.30.1.110 节点的 nodeId是啥?

      这个01f1141d08b5259805b8729fc799ea90125f91fb就是172.30.1.110 节点的 nodeId

      redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave --cluster-master-id 01f1141d08b5259805b8729fc799ea90125f91fb

      执⾏完毕后,从节点就已经被添加完成了。

      我们可以去查看一下

      很好,从节点就已经被添加完成了.

      ### Redis 集群的搭建与配置 #### 一、Redis 集群简介 Redis 是一种高性能的键值存储系统,支持多种数据结构操作。通过集群模式可以实现分布式存储和高可用性。Redis 集群允许多个 Redis 实例协同工作,提供更高的吞吐量和更强的数据持久化能力。 --- #### 二、环境准备 在开始之前,需确认以下条件已满足: - 所有服务器的操作系统版本一致(如 CentOS 7 或 Windows),并安装了相同版本的 Redis 软件。 - 已关闭防火墙或开放必要的端口(默认 Redis 使用 6379 及其衍生端口)。 - 每台服务器上至少有两个 Redis 实例运行,分别作为主节点和从节点[^1]。 --- #### 三、具体步骤 ##### 1. 下载并解压 Redis 文件 下载指定版本的 Redis 压缩包(如 Redis 6.2.5 或更高版本),将其解压到目标路径下。例如,在 Linux 中执行以下命令: ```bash wget http://download.redis.io/releases/redis-6.2.5.tar.gz tar -zxvf redis-6.2.5.tar.gz cd redis-6.2.5 make ``` ##### 2. 创建多个实例目录 为每个 Redis 实例创建独立的工作目录,并复制 `redis.conf` 至对应文件夹中。例如: ```bash mkdir -p /service/redis/{6379,6380} cp redis.conf /service/redis/6379/ cp redis.conf /service/redis/6380/ ``` ##### 3. 修改配置文件 编辑每个实例下的 `redis.conf` 文件,设置不同的监听端口号和其他必要参数。以下是关键配置项: - 设置绑定 IP 地址:`bind 0.0.0.0` - 关闭保护模式:`protected-mode no` - 开启集群功能:`cluster-enabled yes` - 指定集群配置文件位置:`cluster-config-file nodes-{port}.conf` - 设定日志级别:`loglevel notice` ##### 4. 启动 Redis 实例 依次启动各个 Redis 实例。例如: ```bash redis-server /service/redis/6379/redis.conf redis-server /service/redis/6380/redis.conf ``` 如果是在多台物理机上部署,则需要远程登录每台机器重复上述过程[^2]。 ##### 5. 构建集群拓扑 利用 `redis-cli` 的集群管理工具完成初始化操作。假设当前存在六个节点分布在三台主机上,则可输入如下指令构建集群关系: ```bash redis-cli --cluster create \ 192.168.x.y:6379 192.168.x.z:6379 ... \ --replicas 1 ``` 其中 `--replicas` 参数表示每个主节点分配几个副本[^4]。 ##### 6. 验证集群状态 最后可以通过以下方式验证集群是否正常运作: ```bash redis-cli -c -h {任意IP} -p {任一口号} CLUSTER INFO CLUSTER NODES PING SET key value GET key ``` --- #### 四、注意事项 - 如果使用 Docker 容器来部署 Redis 集群,请确保容器间网络互通良好。 - 对于 Windows 平台上的开发测试场景,可通过批处理脚本来简化服务启动流程[^3]。 --- #### 五、示例代码片段 下面展示了一个简单的 Python 程序用于连接至 Redis 集群并向其中写入一条记录: ```python import redis r = redis.StrictRedisCluster(startup_nodes=[{"host": "127.0.0.1", "port": "6379"}], decode_responses=True) r.set('foo', 'bar') print(r.get('foo')) ``` ---
      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值