背景

单点问题:如果一个服务只部署在一台物理服务器上,那这台服务器就是整个服务的 “唯一入口”。一旦它宕机、网络中断或硬件故障,整个服务就会彻底不可用,同时性能也受单机硬件的限制。

单点问题的两大核心痛点:

  1. 可用性(Availability)问题

    • 单机挂了 → 服务直接中断,没有任何冗余备份。

    • 对于线上业务,这意味着用户无法访问、交易中断,甚至造成严重的经济损失。

  2. 性能与并发瓶颈

    • 单机的 CPU、内存、网络带宽都是有限的,能支撑的并发请求数和吞吐量有明确上限。

    • 当业务量增长到单机无法承载时,就会出现响应延迟飙升、服务雪崩等问题。

为什么要引入分布式系统?

引入分布式系统(多节点部署)的核心目标,就是解决单点问题

  • 通过多台服务器共同提供服务,避免 “一台机器挂了全服务挂掉” 的风险。

  • 通过负载均衡,将请求分散到多台机器,突破单机性能瓶颈,提升并发处理能力。

Redis 作为核心的缓存 / 存储组件,如果只部署在单台服务器上,就会成为整个分布式系统的单点瓶颈:

  • 如果 Redis 挂了,所有依赖它的业务服务(如缓存穿透、会话存储)都会受到影响。
  • 单机 Redis 的 QPS 上限(约 10w/s)在高并发场景下很快就会被打满。

因此,我们需要将 Redis 部署成多节点集群,让它为整个分布式系统提供:

  • 更高的可用性:一台节点挂了,其他节点可以顶上。
  • 更高的性能:请求可以分散到多个节点,提升整体吞吐量。
  • 更大的存储容量:数据可以分片存储在多个节点,突破单机内存 / 磁盘限制。

redis经典集群部署方案

1. 主从模式(Master-Slave)

这是 Redis 最基础的高可用部署模式,核心是数据冗余 + 读写分离

  • 核心架构

    • 一个主节点(Master):负责处理所有写请求,并将数据同步到从节点。

    • 多个从节点(Slave):负责复制主节点的数据,处理读请求。

  • 解决的问题

    • 可用性:主节点挂了,从节点可以手动提升为主节点,继续提供服务。

    • 性能:读请求可以负载均衡到多个从节点,分担主节点的读压力。

  • 存在的问题

    • 主节点仍是单点:写操作只能在主节点进行,主节点挂了需要人工干预切换,无法自动故障转移。

    • 数据一致性:主从复制是异步的,可能出现短暂的 “读旧数据”(最终一致性)。

2. 主从 + 哨兵模式(Master-Slave + Sentinel)

在主从模式的基础上,引入了哨兵(Sentinel) 进程,解决了 “主节点挂了需要人工干预” 的问题,实现了自动故障转移

  • 核心架构

    • 哨兵是独立的监控进程,通常部署 3 个或以上,监控所有主从节点的健康状态。

    • 当哨兵检测到主节点宕机时,会自动从从节点中选举一个新的主节点,并更新其他节点的配置。

  • 解决的问题

    • 自动故障转移:主节点挂了,哨兵会自动完成主从切换,无需人工介入,大大提升了可用性。

    • 配置中心:客户端可以连接哨兵,获取当前主节点的地址,实现自动路由。

  • 存在的问题

    • 写瓶颈依然存在:写操作仍然只能在一个主节点上进行,无法水平扩展写能力。

    • 存储容量受限:所有节点存储的都是全量数据,无法分片,存储容量受限于单台服务器的最大内存。

3. Redis 集群模式(Redis Cluster)

这是 Redis 官方提供的分布式解决方案,彻底解决了主从 + 哨兵模式的瓶颈,实现了数据分片 + 高可用

  • 核心架构

    • 数据被分成 16384 个哈希槽(slot),分布在多个主节点上。

    • 每个主节点都有自己的从节点,用于故障转移。

    • 客户端可以直接连接任意节点,根据 key 的哈希值路由到对应的 slot 所在节点。

  • 解决的问题

    • 水平扩展:可以通过增加主节点来线性提升写性能和存储容量。

    • 高可用:内置了故障转移机制,主节点挂了,从节点会自动提升为主节点。

    • 分布式路由:客户端可以自动感知集群拓扑,将请求路由到正确的节点。

从单点 Redis 到 Redis 集群,是一个逐步解决问题的过程:

  1. 单点问题:单机挂了服务就没了,性能也上不去。
  2. 主从模式:解决了读性能和数据冗余问题,但主节点仍是单点,故障需要人工处理。
  3. 主从 + 哨兵:解决了主节点故障的自动切换问题,但写性能和存储容量仍是瓶颈。
  4. Redis 集群:彻底实现了分布式,解决了写性能、存储容量和高可用的所有问题。

主从模式

设计初衷:主从模式是为了 “备份” 和 “读扩展”,不是为了 “多活写入”。

主节点专注写,从节点分担读压力(如查询、统计),这是主从模式的核心价值

28定律:百分之80都是为了读,百分之20为了写,所以主从模式更好的是设立一主多从,并且数据传输是单向的,只能是主到从,设计初衷就是为了读扩展的

  • 数据一致性:所有变更都来自一个源头,避免了多节点同时写导致的数据冲突(比如从节点 A 和从节点 B 同时修改同一个 key,谁对谁错?)。
  • 实现简单:主节点只需要把自己的写操作广播出去,从节点只需要执行,不需要处理复杂的 “冲突解决” 和 “双向同步” 逻辑。

本身的设计里面就只能是主节点给从节点传输数据,这里有个复制缓冲区,只能是主写给从

从节点的数据跟随主节点变化,和主节点保持一致

从节点挂掉:重启后确实能自动从主节点 / 其他从节点重新同步数据,几乎无需人工干预;

主节点挂掉:纯主从模式下(无哨兵 / 集群),需要手动切换从节点为主节点,否则整个集群无法处理写操作 —— 这也是纯主从模式最大的痛点。

主节点挂掉需要手动把某个从节点提升为主节点,或者增加哨兵(后面讲)

简单来说,主从模式大大提高了的并发量和可用性和数据安全性(有多个读节点备份)

对于写不能设立多个主节点,否则主节点之间的数据同步又是一个问题

所以没有解决单点问题中的写,没有解决主节点自动故障转移

配置主从模式

前提:由于没有多台服务器,只有一台,我们需要开多个进程,然后端口号不同

redis-server启动方式

1:临时命令行直接启动

# 启动Redis,指定端口为6380(替换成你想要的端口即可)
redis-server --port 6380
# 后台启动Redis,指定端口6380
redis-server --port 6380 --daemonize yes

2:通过配置文件redis.conf进行修改端口

下次启动的时候指定配置文件即可

要注意区分你启动服务器时的操作是临时的还是永久的,通过配置文件修改就是永久的

搭建主从结构

注意:5.0之前老版本使用slaveof,之后新版本使用replicaof和slaveof都行(建议使用replicaof)

1:直接通过命令指定主节点是谁

# Redis < 5.0
redis-server --port 6380 --slaveof 127.0.0.1 6379 --daemonize yes

# Redis ≥ 5.0(推荐)
redis-server --port 6380 --replicaof 127.0.0.1 6379 --daemonize yes

2:通过配置文件修改

3:客户端命令

# Redis < 5.0
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK

# Redis ≥ 5.0(推荐)
127.0.0.1:6380> REPLICAOF 127.0.0.1 6379
OK

断开所有的主从关系

slaveof no one
replicaof no one
  • 主节点没有 “断开从节点” 的命令:主节点只能被动接受从节点的连接 / 断开,无法主动踢掉从节点,也不能命令从节点升级为主节点;
  • 从节点是主从关系的 “主导方”:只有从节点主动执行 REPLICAOF NO ONE,才能断开和主节点的关系并升级为主节点;
  • 主节点仅能 “感知” 从节点断开:当从节点主动断开后,主节点的 connected_slaves 数量会减少,但主节点本身的角色(master)和数据都不变。

从节点执行断开之后主动变为主节点,数据不会变,但是不会更新之前主节点的数据了

查询当前节点的信息

info replication

Nagle算法

由于redis是tcp连接,tcp协议当中有一个nagel算法

Nagle 算法的核心目的是 减少小数据包的网络传输次数(俗称 “粘包”),通过延迟发送小数据包、凑够一定大小 / 等待确认后再发送,降低网络拥塞;但在 Redis 这类 “低延迟、小数据包频繁传输” 的场景中,Nagle 算法反而会增加延迟,所以 Redis 会默认关闭它。

关闭nagel算法会增加带宽,降低延迟

打开nagel算法会降低带宽,增大延迟

实操演示

三个进程已经启动了,并且都是通过命令配置好了主从关系

前面三个是6379、6380、6381,这三个是进程

由于我们同属于一台服务器,从节点与主节点之间相当于客户端与服务器的通信,从节点需要作为客户端的一个端口与服务器的6379进行连接

图中可以看出是38576和38572两个端口,并且状态时established

当从节点进行set数据的时候根本不允许,因为从节点只能读

主节点的主从信息

role的信息是master,那就是主节点

connected_salve:那就是有两个从节点

offset=1428:这个是复制偏移量,代表主节点和从节点之间数据同步的进度。主从节点都维护这个值,用来判断数据是否同步完成。

lag=表示从节点最后一次向主节点发送确认消息的时间间隔(秒),这里是 0和1秒,说明主从同步很及时。

从节点的主从信息

可以看到role的信息是slave,那就是从节点

主从架构分析

一主一从

对于写请求只能发给主节点

读请求可以发给主节点,也可以发给从节点

可以通过关闭主节点的AOF来缓解一下,因为从节点就是主节点的复制

但是重启的时候不能让主节点直接重启(AOF当中没有数据),否则进一步主从同步的时候,导致从节点的数据也没了

应该把从节点的aof文件拷贝过来,然后让主节点通过这个aof文件重启

一主多从

  • 主节点每修改一条数据,就要把这条数据同时发给所有从节点
  • 从节点越多,主节点的网络带宽压力越大,同步延迟也越长

比较时候从节点<=3的时候

主-从-主

数据同步的链路变长,主从之间的延迟会更大

主从如何实现的???

  • 保存主节点信息

    • 你执行 REPLICAOF 后,Redis 会把主节点的 IP 和端口保存到内部变量(也会写入 redis.conf 持久化)。
    • 下次重启时,从节点会自动读取这些信息,再次尝试连接主节点。
  • 主从建立连接

    • 从节点会主动作为 TCP 客户端,向主节点发起三次握手,建立长连接。
    • 这个连接就是后续所有复制数据的传输通道。
  • 发送 PING 命令

    • 连接建立后,从节点会主动发送 PING 给主节点。
    • 目的是验证主节点是否存活、是否能正常响应,避免在一个不可用的节点上浪费时间。
  • 权限验证

    • 如果主节点设置了密码(requirepass),从节点会自动发送 AUTH 命令进行身份验证。验证失败的话,复制流程会直接终止。

当主节点设置了密码(requirepass 123456),任何客户端(包括从节点)想要和它通信,都必须先执行 AUTH 123456 验证身份,否则主节点会拒绝所有请求。

在主从复制流程中:

1:从节点会自动尝试发送 AUTH 命令,但它需要知道密码是什么;

2:如果从节点没有配置密码,或者配置的密码和主节点不一致,验证就会失败;

3:验证失败后,主节点会断开连接,复制流程直接终止,从节点无法同步数据。

  • 同步数据集

    • 这是最核心的一步,Redis 会根据情况选择:
      • 全量同步:首次连接或数据差异过大时,主节点生成 RDB 文件,发给从节点加载。
      • 增量同步:网络闪断重连时,只同步断开期间的新命令。
    • 整个过程完全自动,不需要你手动生成或传输文件。
  • 命令持续复制

    • 初始同步完成后,主节点会把后续所有写操作,实时通过之前建立的 TCP 连接发给从节点。
    • 从节点收到后,按顺序重放这些命令,保证和主节点的数据一致。

redis提供了一个psync命令,完成数据同步的过程

在完成主从关系的建立之后,从节点自动执行psync命令,不需要我们手动执行,自动向主节点拉取数据

PSYNC 是 Redis 为解决「重连后全量同步效率低」设计的增量同步机制,核心原理是:通过唯一标识(RunID)确认主节点身份,通过复制偏移量(offset)定位数据缺口,通过复制积压缓冲区(backlog)缓存增量数据,优先增量同步,仅在必要时触发全量同步

runID(replid):每个节点启动的时候都会自动生成,只要节点重启就会重新生成

当关系建立好了之后,就需要获取主节点的id

一般来说id2是不用的,但是主和从通信过程当中出现抖动,从会认为主挂掉了,然后把自己升为主节点,然后此时就可以用id2来记录之前的主节点,然后id1自己生成一个,后续如果通信好了又会变回去,这个需要手动干预,但是哨兵模式不需要,哨兵自动完成

字段 含义
repl_backlog_active:1 表示 backlog 缓冲区已启用(1 为启用,0 为关闭)
repl_backlog_size:1048576 缓冲区的总大小,这里是 1MB(1048576 字节),可通过 repl-backlog-size 配置
repl_backlog_first_byte_offset:1 缓冲区中最早数据的起始偏移量,用于判断从节点的 offset 是否还在缓冲区范围内
repl_backlog_histlen:16380 缓冲区中当前已缓存的数据长度(字节数),这里是 16380 字节

这个backlog缓冲区是为了验证offset还是不是在合理范围

  • 主节点每给从节点发 100 字节的复制数据,offset 从 1000 → 1100;
  • 从节点收到后,offset 也从 1000 → 1100;
  • 主节点同时把这 100 字节写入 backlog
  • 若网络断了,主节点继续写数据,offset 涨到 1500,而从节点还停在 1100 → 缺口是 1100~1500。

offset:偏移量,主节点和从节点各自维护一个整数 offset

主节点:记录它总共向从节点发送了多少字节的修改命令。每处理一个写操作,这个数字就会累加

从节点:记录它已经从主节点同步到了哪个位置。

这是主节点自己的总偏移量

然后从节点0要每s上报偏移量给主节点

字段 含义
slave_repl_offset:17094 从节点当前的复制偏移量,表示它已经同步到了第 17094 字节的位置。
master_repl_offset:17094 从节点从主节点那里获取到的、主节点当前的偏移量。
master_replid 从节点当前从属的主节点的 RunID(身份标识)。
  • 数据集合replication id + offset 共同描述了一个唯一的 “数据快照”。
  • 判断逻辑:如果两个 Redis 节点的 replication id 相同,并且 offset 也相同,那么可以认为它们存储的数据是完全一样的。

这里解释的就是唯一性,一般来说id已经唯一了,在加上一个offset更唯一

  • replication id 是 “身份”,用来确认是不是同一个数据源;
  • offset 是 “进度”,用来确认数据同步到了哪一步;
  • 两者结合,PSYNC 就能聪明地决定是 “断点续传”(增量同步)还是 “重新下载”(全量同步)。

流程再度解析

建立好连接之后发送psync命令,根据自身情况发送

如果之前没有链接过主节点,就会发送psync ? -1(?表示无主节点id,-1表示offset)

如果链接过主节点,就会发送psync RunID offset(根据之前的记录发送)

主节点收到之后,进行验证,首先RunID对不对,offset还在不在合理返回(这个通过backlog验证,缺口数据是不是还在backlog当中)

  • 从节点发送 PSYNC <主节点RunID> <从节点当前offset> 给主节点;
  • 主节点验证 RunID 有效,且 offsetbacklog 中 → 回复 +CONTINUE
  • 主节点从 backlog 中读取 offset 之后的所有数据,发给从节点;
  • 从节点接收并执行这些增量数据,同步完成后,继续实时接收主节点的新命令。

如果无id/offset或者id不对(主节点重启)或者offset超出backlog范围(断开太久,缓冲区被覆盖)

  • 从节点发送 PSYNC ? -1(? 表示无 RunID,-1 表示无 offset)给主节点;
  • 主节点回复 +FULLRESYNC <新RunID> <主节点当前offset>,并开始生成 RDB 文件;
  • 主节点生成 RDB 期间,把新收到的写命令写入「复制缓冲区」(避免丢失);
  • 主节点发送 RDB 文件给从节点 → 从节点清空本地数据,加载 RDB;
  • 从节点加载完 RDB 后,主节点把「复制缓冲区」中的新命令发给从节点;
  • 从节点执行这些命令,完成全量同步,后续进入实时增量同步。

注意主节点会fork一个进程,进程去生成RDB,不会写入磁盘,而是以网络形式直接往网络当中写,传给从节点,避免了一系列读硬盘和写硬盘的操作

网络抖动详解

repl-timeout这个就是超时断开

这个是从节点发ping的时间间隔,也就是心跳检测

1. 主节点视角(监控从节点)

主节点会给每个从节点维护一个 “最后交互时间戳”:

  • 每次给从节点发命令、收到从节点的 PONG 响应,都会更新这个时间戳;

  • 如果某个从节点的 “当前时间 - 最后交互时间戳” > repl-timeout,主节点会:

    1. 标记该从节点状态为 down

    2. 断开与该从节点的 TCP 复制连接;

    3. 在日志中打印 Replica xxx:xxx timed out, closing connection

2. 从节点视角(监控主节点)

从节点也会监控与主节点的交互:

  • 从节点会定期给主节点发 PING(默认每 10 秒一次),并等待主节点的响应;

  • 如果从节点超过 repl-timeout 秒没收到主节点的任何数据(包括 PONG 响应、复制命令),会:

    1. 标记主节点状态为 down

    2. 断开与主节点的 TCP 连接;

    3. 尝试重新连接主节点(默认会无限重试,间隔由 replica-retry-timeout 控制);

    4. 日志中打印 Master xxx:xxx timed out, reconnecting...

需要打开才能使用,否则主从之间感受不到对方的失联,就会占用资源,并且哨兵模式也用不了

当主节点挂掉,就会导致写操作无法进行,主从也会随之断开,因为超过了60s无响应

  • 从节点检测到主节点失联(触发 repl-timeout),会断开连接并反复重试重连主节点(默认无限重试,间隔由 replica-retry-timeout 控制,默认 60 秒);
  • 从节点始终保持 “从节点身份”,不会主动切换为 “主节点”,依然是只读状态;
  • 此时整个集群:写操作完全中断(主节点失联),读操作只能用从节点的旧数据。
# 1. 进入从节点的客户端
redis-cli -p 6380

# 2. 断开与旧主节点的复制关系,升级为主节点
127.0.0.1:6380> REPLICAOF NO ONE
OK

# 3. (可选)让其他从节点(如果有)切换到新主节点
redis-cli -p 6381 REPLICAOF 127.0.0.1 6380

纯主从架构下(无哨兵/无集群),从节点不会自动升主,必须手动操作;只有搭配哨兵(Sentinel)或 Redis Cluster 时,才会自动触发主从切换

注意:这里有一个小知识点

一般来说Linux有多用户,不要使用root去运行进程,因为root的权限最高,运行进程,就会导致被攻击时,别人能使用redis这个进程去读写任何文件

一般使用别的用户去运行,比如这里redis下载好了之后会自动装一个redis用户

vim /etc/passwad

  • redis:用户名;
  • x:密码占位符(密码存在 /etc/shadow);
  • 983:983:UID(用户 ID)和 GID(组 ID);
  • Redis Database Server:用户描述;
  • /var/lib/redis:用户的家目录;
  • /sbin/nologin:登录 shell(禁止登录)。

用专用的 redis 用户运行,它只能访问 Redis 数据目录、日志目录,就算被攻击,危害也被限制在极小范围内。

也就是降低你的权限

哨兵模式

背景:主要解决主节点挂了 / 网络断了 → 自动把从节点提升为主节点,让服务继续可用

对于主从模式,当主节点挂了,整个集群就无法写入,此时需要人工手动干预

并且主从模式下,主节点故障后,你都不知道,没有感知到故障,哨兵能够解决24小时监控

还能解决主恢复后,旧主和新主的问题

  • 监控:主节点活着吗?
  • 自动选主:主死了,从里面选一个最好的从节点升为主
  • 自动配置:通知其他从节点同步新主,旧主变从

通过自动化的方式,解决主节点挂机问题,弥补主从模式缺陷(人工操作)

哨兵(Sentinel)进程就是 Redis 官方原生提供的核心组件

基础原理

对于后端开发,很多服务器都需要监控,保证高可用,不可能是人工24小时监控,所以写一个进程来监控,第一时间发现异常就会报警

redis提供了多个sentinel哨兵,这些都是单独的进程,每个sentinel都去监控数据节点

注意是一个哨兵监控其他哨兵和所有的数据节点(主节点和从节点),每个哨兵都是这样

当从节点挂了,其实没关系

1:当主节点挂了,哨兵检测到了,需要结合其他多个哨兵,防止出现误判

2:当很多哨兵都认为主节点挂了,那就会在哨兵当中推举出一个leader,然后这个leader负责从现有的从节点当中,挑选一个作为新的主节点

3:挑选出的主节点之后,哨兵节点,就会自动控制被选中的节点,执行replicaof no one,并且控制其他从节点,修改replicaof 到新的主节点上

4:哨兵节点会自动的通知客户端程序,告知新的主节点是谁,并且后续客户端在进行写操作,就会针对新的主节点进行操作了

哨兵节点的配置:

基本原则:避免单点问题,最好配置奇数个

本质来说一个哨兵节点也可以完成

但是会增加误判概率,并且可能哨兵也会挂掉,所以应当配置一定数量的哨兵节点

所以,通过以上来看,哨兵具备以下功能

1:监控

2:自动的故障转移

3:通知

配置哨兵模式

一般来说,这些节点需要配在不同的服务器当中,否则一台服务器挂掉,直接全部挂掉,这是不现实的,当前我们的配置又不具备

但是可以进行配置试试,但由于每个配置文件都在一台服务器,很繁琐

所以可以使用docker,详细的docker操作请看这篇文章

https://blog.csdn.net/Laydya/article/details/152094224

这里已经全部停掉redis服务,接下来采用docker来配置哨兵模式

docker pull拉取镜像

查看本地镜像,看看有没有拉取成功

配置yml,后续docker启动的时候可以根据配置文件一键启动

告诉 Docker 如何创建网络、拉取 Redis 镜像、启动主节点 / 从节点 / 哨兵容器,并配置容器的网络、端口、数据持久化和启动规则,最终一键搭建出可运行的 Redis 哨兵集群

redis.conf按照之前的主从模式进行好配置,记得打开心跳

yml文件也要根据路径具体配置docker的启动

哨兵配置        

注意有两种心跳包,一个是在redis.conf中的,这是主节点和从节点之间的心跳,目的是维持主从复制关系,和哨兵无关

还有一个是在哨兵配置当中的,这是哨兵对主节点 / 从节点 / 其他哨兵的检测心跳,目的是判断节点是否存活,是哨兵触发故障转移的核心

在docker-compose.yml下启动

可以看到全都启动起来了

如果起不起来,可能是版本之间,yml配置问题,需要根据docker日志一步步排查

看一下是否处于同一个局域网

检查完毕,同属于一个局域网

测试哨兵模式

左屏幕一直打印哨兵日志,右屏幕让主节点挂掉

sdown是主观下线,本哨兵节点认为主节点下线

odown是客观下线,好几个哨兵节点认为主节点下线,达到了法定票数,也就是2

此时就需要选举一个新的节点作为主节点

流程

哨兵节点一直通过心跳包监控,判定redis服务器是否正常工作

如果心跳包没有如约而至,说明节点挂掉了

从节点挂掉后,哨兵只会做 “标记 + 记录”,但不会触发任何故障转移(选举新主),只有主节点挂掉才会触发 odown 和故障转移

主节点挂掉后,此时是主观下线,为sdown,可能因为网络抖动、主节点短暂卡顿导致,所以不会直接触发选举。会向集群内其他哨兵发 “投票请求”:“我觉得主节点挂了,你们觉得呢?”;

当同意的数量大于等于你配置文件中的法定票数之后,标记为odown客观下线

这是集群级别的共识判断,是触发故障转移(选举新主)的核心前提

注意:当然也可能是服务器没有挂掉,因为网络抖动很严重导致所有的哨兵都接收不到心跳包,但是哨兵都接收不到,客户端也接收不到了,这样来说服务器也可以认为挂掉了

挂不一定是进程挂掉,只要无法正常访问

接下来就是投票选举新的主节点

当主节点被标记为 odown(客观下线)后,所有监控该主节点的哨兵都会尝试成为领导者,来执行故障转移。

选举规则(Raft 算法简化版)

  • 发起投票:每个哨兵都会给自己投一票,并向其他哨兵发送请求:“请选我当领导者”。

  • 投票规则:一个哨兵在一轮选举周期内,只会给第一个向它发起请求的哨兵投票。

  • 当选条件:一个哨兵如果获得了超过半数N/2 + 1)的哨兵选票,它就会成为领导者。

    • 例如:3 个哨兵,获得 2 票即可当选;5 个哨兵,获得 3 票即可当选。

  • 超时重试:如果一轮选举超时(默认 10 秒)没有产生领导者,所有哨兵会等待一段随机时间后,发起新一轮选举,避免冲突。

领导者的任务

当选的哨兵领导者,就是这次故障转移的 “总指挥”,它负责:

  • 从所有从节点中,挑选出最合适的一个作为新主。

  • 向其他节点(从节点、客户端)发布新主的信息。

注意投票时先投给自己,如果别人的请求先到时还未投给自己(自己还没发现主节点挂掉),那此时投给别人,极端情况就是所有都投给自己,那会超时重试,那所有的哨兵都会

随机等待一段时间,此时就会出现别的请求先来,避免死局

领导者确定后,就开始从所有健康的从节点里,按以下优先级进行筛选和排序:

1. 第一轮筛选:排除不合格的从节点

领导者会先排除掉那些不适合当主的从节点:

  • 已经标记为 sdown(主观下线)的从节点。

  • 与旧主节点断开连接时间超过 down-after-milliseconds 5 倍的从节点(认为数据太旧)。

2. 第二轮排序:按优先级打分

对于剩下的合格从节点,领导者会按以下优先级进行排序,得分最高的就是新主:

  1. 优先级(replica-priority)

    • 每个从节点都有一个优先级配置(replica-priority,默认 100)。优先级数字越小,优先级越高。

    • 这是最高权重的考量。如果你想指定某个从节点优先成为新主,可以把它的优先级设得比其他节点低。

  2. 复制偏移量(replication offset)

    • 如果优先级相同,就比较复制偏移量。偏移量越大,说明这个从节点同步的数据越新,越接近旧主节点。

    • 这是为了保证数据丢失最少。

  3. 运行 ID(Run ID)

    • 如果优先级和偏移量都相同,就比较从节点的 Run ID。字典序较小的那个会被选中。

    • 这一步纯粹是为了打破平局,保证选举结果的确定性。

3. 执行切换

新主确定后,领导者会执行以下操作:

  1. 向被选中的从节点发送 SLAVEOF NO ONE 命令,让它脱离旧主,升级为独立的主节点。

  2. 向其他所有从节点发送 SLAVEOF <新主IP> <新主端口> 命令,让它们去同步新主。

  3. 更新自己的配置,并通过 __sentinel__:hello 频道向其他哨兵和客户端广播新主的地址。

数据容量这里的意思:假设你的内存只有100g,但是你存储的数据有200g怎么办?因为redis是内存型的数据库,所有的数据都在内存,所以这里需要解决容量问题,redis集群就可以

还有单点写问题,你是提高了高可用,但是没有根本的解决单点写的性能瓶颈

很明显,解决方案就是加入多个写节点,这样数据容量也上来了

源码分析

// 哨兵启动入口(替代普通 redis-server 的 main 函数)
int sentinelMain(int argc, char **argv) {
    // 1. 切换运行模式为哨兵(设置 server.sentinel_mode = 1)
    server.sentinel_mode = 1;
    // 2. 加载哨兵配置文件(sentinel.conf),解析监控的主节点配置
    loadSentinelConfigFile(server.configfile);
    // 3. 初始化定时任务:核心是 3 个定时事件
    aeCreateTimeEvent(server.el, 1000, sentinelTimerProc, NULL, NULL); // 1秒检测主节点心跳
    aeCreateTimeEvent(server.el, 10000, sentinelReconfiguratorTimer, NULL, NULL); // 10秒同步从节点/哨兵状态
    aeCreateTimeEvent(server.el, 1000, sentinelHelloTimer, NULL, NULL); // 1秒往 __sentinel__:hello 发广播
    // 4. 启动事件循环(和普通 Redis 一致)
    aeMain(server.el);
    return 0;
}

核心逻辑:哨兵本质是特殊模式的 Redis 服务器,启动后会初始化 3 个核心定时任务,这是所有监控 / 发现 / 选举的基础。


// 1秒执行一次:检测主节点存活状态
void sentinelTimerProc(aeEventLoop *el, long long id, void *clientData) {
    sentinelCheckMasterDownState(); // 核心:判定主节点是否 sdown
}

// 判定主观下线的核心逻辑
void sentinelCheckMasterDownState(sentinelRedisInstance *mi) {
    // 1. 计算主节点无响应时间(当前时间 - 最后一次收到 PONG 的时间)
    mstime_t delay = mstime() - mi->last_pong_time;
    // 2. 对比 down-after-milliseconds 配置(你设置的 30000ms)
    if (delay > mi->down_after_period) {
        // 3. 标记为主观下线(sdown=1)
        mi->s_down = 1;
    } else {
        mi->s_down = 0;
    }
}

核心逻辑:每秒检测主节点最后一次响应时间,超过阈值则标记 s_down=1(主观下线),这是故障转移的起点。

// 1秒执行一次:往主节点的 __sentinel__:hello 频道发广播
void sentinelHelloTimer(aeEventLoop *el, long long id, void *clientData) {
    // 1. 构造广播消息:包含哨兵IP、端口、运行ID、监控的主节点信息
    robj *hello = createHelloMessage(mi);
    // 2. 发布到主节点的 __sentinel__:hello 频道
    redisAsyncCommand(..., "PUBLISH __sentinel__:hello %s", hello->ptr);
    // 3. 订阅该频道,接收其他哨兵的广播
    redisAsyncCommand(..., "SUBSCRIBE __sentinel__:hello", ...);
}

// 收到其他哨兵广播后的处理逻辑
void sentinelHelloMessageCallback(redisAsyncContext *c, void *reply, void *privdata) {
    // 解析广播中的哨兵信息,添加到自己的哨兵列表
    sentinelAddSentinel(mi, ip, port, runid);
    // 更新 num-other-sentinels 计数
    mi->num_other_sentinels = listLength(mi->sentinels) - 1;
}

核心逻辑:哨兵通过主节点的 Pub/Sub 机制广播自身信息,同时订阅频道接收其他哨兵的信息,自动完成集群发现 —— 这也是你之前 num-other-sentinels=0 的根源:该频道的消息收发失败,哨兵无法解析彼此的信息。

注意1:这个频道本质就是所有的哨兵跟主节点进行tcp连接,主节点进行转发消息,比如

# 哨兵1 发布的消息(RESP 格式)
PUBLISH __sentinel__:hello "192.168.32.6,26379,5f987a6b...,redis-master,192.168.32.2,6379,0"

然后主节点就会转发这个哨兵1的消息到频道里面(也就是把这个消息转发给其他哨兵),这样哨兵之间就能够通信,这样能够减少tcp连接的条数,否则所有的哨兵都要与别的哨兵tcp连接,这样的设计的好处就是避免网状连接的复杂度,当连接过多管理成本就过大

注意2:哨兵先和主节点建立tcp连接,然后订阅频道,然后info replication获取与主节点连接的从节点,然后哨兵和从节点tcp连接,去监测从节点

// 判定客观下线:统计认为主节点 sdown 的哨兵数量
int sentinelCheckObjectivelyDown(sentinelRedisInstance *mi) {
    int count = 0;
    // 遍历所有哨兵,统计 s_down=1 的数量
    listIter li; listNode *ln;
    listRewind(mi->sentinels, &li);
    while((ln = listNext(&li))) {
        sentinelRedisInstance *si = ln->value;
        if (si->flags & SRI_MASTER_LINK_DOWN) count++;
    }
    // 数量 ≥ quorum(你配置的 2),则标记 odown=1
    return count >= mi->quorum;
}

// 领导者选举核心:先自我锁票,再拉票
int sentinelElectLeader(sentinelRedisInstance *mi) {
    // 1. 自我锁票:给自己投一票
    mi->leader_runid = server.runid;
    mi->leader_epoch = mi->current_epoch;
    // 2. 向其他哨兵发送拉票请求
    sentinelRequestVote(mi);
    // 3. 统计票数,超过半数则当选
    return mi->votes >= (listLength(mi->sentinels)/2 + 1);
}

核心逻辑:客观下线需要满足 sdown 哨兵数 ≥ quorum;选举采用 Raft 简化算法,先自我锁票,再拉票,超过半数即当选。

// 故障转移状态机:选新主 → 切换从节点 → 更新配置
void sentinelFailoverStateMachine(sentinelRedisInstance *mi) {
    switch(mi->failover_state) {
        case SENTINEL_FAILOVER_STATE_SELECT_SLAVE:
            // 选新主:按优先级 → 偏移量 → RunID 排序
            sentinelSelectSlave(mi); 
            break;
        case SENTINEL_FAILOVER_STATE_PROMOTE_SLAVE:
            // 提升从节点为新主:发送 SLAVEOF NO ONE
            sentinelPromoteSlave(mi);
            break;
        case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
            // 让其他从节点同步新主:发送 SLAVEOF 新主IP 端口
            sentinelReconfigureSlaves(mi);
            break;
    }
}

核心逻辑:故障转移分 3 步,严格按优先级选新主,确保数据丢失最少。

  • 解决的问题:核心解决了主从架构 “人工故障转移” 的问题,实现了主节点的高可用;
  • 未解决的问题
    • 核心:主节点单点写问题仍存在,写压力无法分担;
    • 其他:数据一致性风险、脑裂、无分片能力、扩容复杂等。

集群模式

广义的集群:只要是多个机器构成的分布式系统,都可以说是集群,前面的主从,哨兵都是

狭义的集群:redis提供的集群模式

Redis Cluster(Redis 集群)是 Redis 官方提供的分布式解决方案,它的核心目标就是解决哨兵模式(主从 + 哨兵)的核心缺陷 —— 尤其是 “单点写” 和 “单节点存储瓶颈” 问题。

架构模式 核心能力 核心缺陷
单机 Redis 基础读写、存储 单点故障、单点写、存储上限受限
主从 Redis 数据备份、读负载分担 主节点单点写、故障需人工切换
主从 + 哨兵 主节点高可用(自动切换) 主节点单点写、无分片、存储上限受限
Redis Cluster 分布式分片 + 多主高可用 不支持强一致性、部分命令受限

基础原理

集群模式中:每个主节点对应多个从节点,也就是从节点是专属于某个主节点的

那此时就需要想一想:主节点是写,是数据的来源?那数据应该如何分配?是主节点之间同步?还是每个主节点存储的不一样的数据

如果采用主节点之间同步数据,那每一时刻也只能有一个主节点写,因为如果同时写就会导致数据冲突,那这样就会回到单点写问题,而且写完之后同步又浪费io资源,而且存储数据的内存大小还是没变

综上,采用数据分片,每个主节点存储不同的数据

主节点采用分片,一个主节点有多片

方案一:哈希求余

当key存储的时候,借助hash函数(md5)%N(片数),存储到不同的主节点当中

缺陷:当扩容的时候,每次引入新的主节点的时候,会导致原来数据计算出不在原来的片位,此时就会导致大量的数据需要重新计算,此时消耗的成本也很高

方案二:一致性哈希算法

直接一开始%一个很大的数字

缺陷:数据不均匀

方案三:哈希槽分区算法

槽位号 = CRC16(key) % 16384
  • CRC16:是一种轻量的哈希算法,输出 16 位整数(0~65535),计算速度极快(适配 Redis 单线程高性能需求);
  • % 16384:把 CRC16 的结果限定在 0~16383 范围内,确保每个 key 都能映射到唯一的槽位。

首先划分为16384个槽位,这个是固定不变的,不随着主节点的增加而增加

redis还会维护一张槽位-节点的映射表

  • 槽位 0~5460 → 主节点 A;
  • 槽位 5461~10922 → 主节点 B;
  • 槽位 10923~16383 → 主节点 C;
// Redis 源码中 clusterNode 结构体的核心字段
typedef struct clusterNode {
    // 16384位的位图,每一位对应一个槽位(0~16383)
    // 位值=1 → 该槽位归当前节点管理;位值=0 → 不归当前节点管理
    unsigned char slots[16384/8]; // 16384位 = 2048字节(仅2KB)
    int numslots; // 当前节点负责的槽位总数
} clusterNode;

每个主节点都有一个这样的slots位图,拥有哪个槽位,位图就是1

注意槽位也可以离散管理,节点可以拥有离散的槽位

这样的好处就是key-槽位-节点,把key和节点的映射关系进行解耦,也就是解决了方案一和方案二的缺陷,当数据迁移的时候,仅仅需要修改槽位和节点的映射关系,无需修改key的计算,然后进行部分数据搬运即可,并且分配也很均匀,每个节点基本槽位一样,你如果使用一致性hash,一开始分配可能是畸形的

问题一:集群最多有16384片吗?

Redis 官方明确不建议把集群节点数做到 16384 个,核心原因有 3 点:

  • 集群元数据同步开销:每个节点需要维护「槽位 - 节点」映射表(2KB),但节点间通过 Gossip 协议同步状态时,消息量随节点数呈线性增长(1000 个节点的 Gossip 消息量已是 3 个节点的 300 倍),会占用大量网络 / CPU 资源;
  • 运维复杂度:16384 个节点的集群几乎无法运维(故障排查、扩缩容、监控都不可行);
  • 性能瓶颈:Redis Cluster 的优势是 “分片 + 高可用”,而非 “海量节点”—— 节点数过多会导致路由延迟增加(客户端需缓存更多「槽位 - 节点」映射),反而降低性能。
场景 主节点数建议 每个节点槽位数 核心目的
小型集群(测试 / 单机) 3 个 5461/5461/5462 最小高可用集群
中型集群(生产) 8~32 个 500~2000 个 / 节点 平衡性能与扩展性
大型集群(超大规模) ≤100 个 ≥163 个 / 节点 避免 Gossip 协议过载

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

Cluster 里主节点 ↔ 主节点 的 Gossip 心跳→ 只为集群状态同步(谁活着、槽位在哪、谁宕机)

16384是2kb,主节点之间的心跳需要传给对方,如果是8kb,看起来不多,但是心跳需要一直发,长时间来看也吃网络带宽资源的,元数据的内存 + 网络开销极致小

并且hash计算槽位的时候非常快

配置集群模式

记得清除redis容器

3主3从

Docker 容器内的文件默认是临时的,需要把配置 / 数据挂载到宿主机目录,避免容器删除后数据丢失;按节点分目录更清晰。

# 创建总目录,后续每个节点的配置/数据都放在这里
mkdir -p /usr/local/redis-cluster/{node1,node2,node3,node4,node5,node6}
cd /usr/local/redis-cluster

Redis Cluster 启动必须配置集群相关参数,且每个节点需要唯一的端口 / IP 标识。

每个都需要编写一个redis.conf文件,编辑 node1/redis.conf 文件:

# 基础网络配置
port 6379  # 节点端口(容器内端口,可统一用6379,宿主机映射不同端口)
bind 0.0.0.0  # 允许所有IP访问,容器内必须配置,否则集群节点无法通信
protected-mode no  # 关闭保护模式,否则外部无法连接
daemonize no  # 禁止后台运行(Docker容器内必须关闭,否则容器启动后会立即退出)

# 持久化配置(可选但建议开启,避免集群重启数据丢失)
appendonly yes  # 开启AOF持久化
appendfsync everysec  # 每秒刷盘,平衡性能和数据安全

# 集群核心配置(必须开启)
cluster-enabled yes  # 开启集群模式(核心开关,没有这个就是普通Redis)
cluster-config-file nodes.conf  # 集群元数据文件(自动生成,记录槽位/节点信息)
cluster-node-timeout 5000  # 节点超时时间(5秒),超时未响应则判定节点下线
cluster-announce-ip 172.17.0.2  # 容器IP(后续启动容器时固定分配,关键!)
cluster-announce-port 6379  # 客户端连接端口
cluster-announce-bus-port 16379  # 集群总线端口(Gossip协议通信,端口=服务端口+10000)
# 复制配置到node2-node6,仅修改cluster-announce-ip(后续容器IP依次为172.17.0.3~172.17.0.7)
sed 's/172.17.0.2/172.17.0.3/' node1/redis.conf > node2/redis.conf
sed 's/172.17.0.2/172.17.0.4/' node1/redis.conf > node3/redis.conf
sed 's/172.17.0.2/172.17.0.5/' node1/redis.conf > node4/redis.conf
sed 's/172.17.0.2/172.17.0.6/' node1/redis.conf > node5/redis.conf
sed 's/172.17.0.2/172.17.0.7/' node1/redis.conf > node6/redis.conf
  • cluster-enabled yes:开启集群模式,Redis 进程会加载集群相关逻辑(槽位管理、Gossip 协议等);
  • cluster-announce-ip:容器内的 IP 地址,集群节点间通过这个 IP 通信(Docker 默认网桥网段是 172.17.0.0/16,后续启动容器时手动分配这个 IP,避免自动分配导致 IP 变化);
  • cluster-announce-bus-port:集群节点间通信的总线端口,用于 Gossip 心跳、槽位迁移等,必须和服务端口错开且唯一。

Docker 默认的桥接网络会随机分配 IP,为了让cluster-announce-ip固定,需要创建自定义网络:

# 创建自定义网桥,指定网段(和配置文件中的IP匹配)
docker network create --subnet=172.17.0.0/16 redis-cluster-net

自定义网络可以手动指定容器 IP,确保配置文件中的cluster-announce-ip和容器实际 IP 一致,否则集群节点间无法通信。

依次启动每个节点的容器,挂载宿主机配置 / 数据目录,指定固定 IP:

docker run -d \
  --name redis-node1 \
  --net redis-cluster-net \
  --ip 172.17.0.2 \
  -v /usr/local/redis-cluster/node1/redis.conf:/etc/redis/redis.conf \
  -v /usr/local/redis-cluster/node1/data:/data \
  -p 6379:6379 -p 16379:16379 \
  redis:5.0.9 \
  redis-server /etc/redis/redis.conf
# node2(主节点2,IP=172.17.0.3)
docker run -d --name redis-node2 --net redis-cluster-net --ip 172.17.0.3 -v /usr/local/redis-cluster/node2/redis.conf:/etc/redis/redis.conf -v /usr/local/redis-cluster/node2/data:/data -p 6380:6379 -p 16380:16379 redis:5.0.9 redis-server /etc/redis/redis.conf

# node3(主节点3,IP=172.17.0.4)
docker run -d --name redis-node3 --net redis-cluster-net --ip 172.17.0.4 -v /usr/local/redis-cluster/node3/redis.conf:/etc/redis/redis.conf -v /usr/local/redis-cluster/node3/data:/data -p 6381:6379 -p 16381:16379 redis:5.0.9 redis-server /etc/redis/redis.conf

# node4(从节点1,IP=172.17.0.5)
docker run -d --name redis-node4 --net redis-cluster-net --ip 172.17.0.5 -v /usr/local/redis-cluster/node4/redis.conf:/etc/redis/redis.conf -v /usr/local/redis-cluster/node4/data:/data -p 6382:6379 -p 16382:16379 redis:5.0.9 redis-server /etc/redis/redis.conf

# node5(从节点2,IP=172.17.0.6)
docker run -d --name redis-node5 --net redis-cluster-net --ip 172.17.0.6 -v /usr/local/redis-cluster/node5/redis.conf:/etc/redis/redis.conf -v /usr/local/redis-cluster/node5/data:/data -p 6383:6379 -p 16383:16379 redis:5.0.9 redis-server /etc/redis/redis.conf

# node6(从节点3,IP=172.17.0.7)
docker run -d --name redis-node6 --net redis-cluster-net --ip 172.17.0.7 -v /usr/local/redis-cluster/node6/redis.conf:/etc/redis/redis.conf -v /usr/local/redis-cluster/node6/data:/data -p 6384:6379 -p 16384:16379 redis:5.0.9 redis-server /etc/redis/redis.conf
  • --net redis-cluster-net:指定容器使用自定义网络,确保 IP 固定;
  • --ip 172.17.0.X:手动分配 IP,和配置文件中的cluster-announce-ip一致;
  • -v 宿主机目录:容器目录:挂载配置文件和数据目录,确保配置持久化、数据不丢失;
  • -p 宿主机端口:容器端口:映射端口(容器内都是 6379/16379,宿主机用不同端口避免冲突);
  • redis:5.0.9:指定 Redis 版本,和你的环境一致;
  • redis-server /etc/redis/redis.conf:启动 Redis 并加载自定义配置(默认启动是无集群配置的普通 Redis)。
docker ps  # 查看6个容器是否都是Up状态

有时候可能是配置的网段被占用了,需要更换成别的网段

Redis 5.0+ 内置了redis-cli --cluster命令(5.0 之前需要用redis-trib.rb,依赖 Ruby,更复杂),直接用这个命令初始化集群。

进入任意一个节点

docker exec -it redis-node1 /bin/bash
redis-cli --cluster create \
  172.17.0.2:6379 172.17.0.3:6379 172.17.0.4:6379 \  # 3个主节点
  172.17.0.5:6379 172.17.0.6:6379 172.17.0.7:6379 \  # 3个从节点
  --cluster-replicas 1  # 每个主节点分配1个从节点

配置的时候就发现网段被占用了,所以进行了更换

  • 终端会输出集群规划(比如 node1/node2/node3 是主节点,分别负责 0-5460、5461-10922、10923-16383 槽位;node4 是 node1 的从节点,node5 是 node2 的从节点,node6 是 node3 的从节点);
  • 输入yes确认集群规划,等待初始化完成。

接下来验证集群

# 进入容器后,连接集群(-c表示集群模式,自动路由槽位)
redis-cli -c -h 172.17.0.2 -p 6379

# 查看集群节点信息
172.17.0.2:6379> CLUSTER NODES
# 写入一个key,Redis会自动路由到对应槽位的主节点
172.17.0.2:6379> SET testkey testvalue
-> Redirected to slot [5798] located at 172.17.0.3:6379  # 自动路由到node2
OK

# 读取key(无论连接哪个节点,都能读到)
172.17.0.2:6379> GET testkey
"testvalue"

# 查看key所属槽位
172.17.0.2:6379> CLUSTER KEYSLOT testkey
(integer) 5798

此时你如果使用-c就是连接集群,那此时就是一个整体,无论你连接哪个节点都是一样的,会进行路由

Redis Cluster 实现了智能路由,整个过程分 3 步(你不用手动干预,Redis 自动完成):

1. 客户端连接任意节点后,会先获取「槽位 - 节点」映射表

当你用 redis-cli -c 连接某个节点时,该节点会把集群的「16384 个槽位分别对应哪个主节点」的映射表,返回给客户端并缓存;

2. 客户端计算 key 所属槽位,自动路由到对应主节点

比如你执行 SET testkey 123

  • 客户端先计算 testkey 的槽位:CRC16(testkey) % 16384 = 5798

  • 客户端查缓存的映射表,发现槽位 5798 属于 node2(172.19.0.3);

  • 客户端自动把请求转发到 node2 执行,你会看到终端输出 -> Redirected to slot [5798] located at 172.19.0.3:6379

3. 读操作同理,写操作只到主节点,读操作可到从节点(默认读主)

  • 写操作(SET/DEL/HSET 等):必须路由到 key 所属槽位的主节点执行(保证数据一致性);

  • 读操作(GET/HGET 等):默认路由到主节点,也可通过 READONLY 命令让客户端优先读从节点(分担主节点压力)。

Redis Cluster 能成为 “整体”,核心靠 2 类通信

1. 主节点之间的 Gossip 通信(集群状态同步)

  • 每个主节点每秒随机给几个同伴发 Gossip 包,内容包括:✅ 自己的状态(活 / 死);✅ 槽位分配情况;✅ 其他节点的状态(比如检测到某个节点失联);

  • 作用:确保所有主节点都知道 “整个集群的拓扑结构”,比如 node1 知道 node2 负责 5461-10922 槽位,node3 知道 node1 的从节点是 node4。

2. 主从节点之间的复制通信(数据同步)

  • 每个主节点的从节点(比如 node1 的从节点是 node4),会和主节点建立 “复制连接”;

  • 主节点有数据变更(比如 SET/DEL),会实时把变更同步给从节点;

  • 作用:主节点宕机后,从节点能立即升级为主节点,数据不丢失。

注意点

1. 必须加 -c 参数才能自动路由

如果连接时没加 -c(比如 redis-cli -h 172.19.0.2 -p 6379),客户端不会自动路由,执行跨槽位操作会报错 MOVED 5798 172.19.0.3:6379,需要你手动连接目标节点;

2. 写操作只能到主节点,从节点默认只读

从节点(比如 node4、node5、node6)默认拒绝写操作,执行 SET 会报错 READONLY You can't write against a read only replica.,这是为了避免数据不一致。

由于先发生路由错误,所以先报了moved

如果采用了-c,那就会重定向给主节点

集群深度刨析

当从节点挂了,没事

当主节点挂了,因为主节点才能写,从不能写

此时所做的工作就和之前的哨兵一样,下线主节点,选一个新的主节点出来(故障转移)

假设集群是 3 主 3 从:node1(主,172.19.0.2)node4(从,172.19.0.5)node2(主)node5(从)node3(主)node6(从)

在宕机之前正常通信,gossip协议,不止是简单的心跳,还包含了集群的快照信息

包类型 发送时机 核心作用
PING 每个节点每秒随机发给几个节点 1. 检测对方是否存活;2. 携带自己的节点状态 + 集群信息
PONG 收到 PING 包的节点立即回复 1. 告诉对方 “我还活着”;2. 回传自己的最新集群信息
PFAIL 节点判定其他节点 “主观下线” 时发送 通知集群:“我认为 XX 节点挂了(主观下线)”
FAIL 集群判定节点 “客观下线” 时发送 通知集群:“XX 节点已被多数主节点确认下线(客观下线)”

每种类型的包都有特殊的字段,这个可以自行查资料学习

假设现在 node1 宕机:

步骤 1:节点失联,触发「主观下线(PFAIL)」

  • 集群内所有主节点每秒通过 Gossip 协议互发心跳包;(每s不是都发,是挑一些发,否则这样全发的成本太高,保底是太久没联系必发)

  • node2/node3 超过 cluster-node-timeout(5 秒)没收到 node1 的心跳响应,就会把 node1 标记为「主观下线」(PFAIL,即 “我认为它挂了”);

  • 👉 大白话:node2 心里想 “node1 5 秒没回我消息,我觉得它挂了”,但不会立刻行动(避免单节点误判)。

步骤 2:Gossip 传播,触发「客观下线(FAIL)」

  • node2/node3 会把 “node1 主观下线” 的信息,通过 Gossip 包同步给集群所有节点;

  • 超过半数主节点(这里是≥2 个:node2+node3)都认为 node1 下线,node1 会被标记为「客观下线(FAIL)」(即 “大家都认为它挂了”);

  • 👉 大白话:集群达成共识 “node1 真的宕机了,必须处理”。

如果是从节点挂掉,那不会进行故障转移

如果是主节点挂掉,那需要进行故障转移,寻找宕机的主节点的从节点进行故障转移

步骤 3:从节点竞选 “新主节点”

  • node1 的从节点 node4 发现主节点被标记为客观下线,会发起 “竞选投票”;

  • 集群内所有主节点(node2、node3)会给 node4 投票:

    • 只有「持有槽位的主节点」有投票权;

    • 每个主节点只能投 1 票,超过半数(≥2 票)则竞选成功;

  • 👉 大白话:node4 举手 “我要当新主”,node2、node3 都同意,node4 竞选成功(如果有多个从节点,会按「复制偏移量(数据同步进度)+ 运行时长」排序,数据最新的优先)。

竞选这里注意:

如果有多个从节点,那会进行休眠,休眠时间 = 500ms + 随机值(0 ~ 500ms) + (从节点优先级 × 1000ms)

休眠时间最短 → 复制偏移量最高 → 运行时长最长 → 节点 ID 字典序小;

其他主节点投票时,早就判断哪个offset最好,就只投给它

休眠时间 = 避免冲突

offset = 决定谁能赢

步骤 4:从节点升级为主节点,接管槽位

  • node4 成功当选后,会执行 3 个关键操作:

    1. 把自己的角色从「slave」改为「master」;

    2. 接管原主节点 node1 的所有槽位(0-5460);

    3. 通过 Gossip 协议向集群广播:“我现在是新主,槽位 0-5460 归我管”;

  • 👉 大白话:node4 官宣 “以后我就是新主,node1 的活我全包了”。

步骤 5:客户端自动路由,故障转移完成

  • 集群所有节点更新槽位映射表(槽位 0-5460 现在对应 node4);

  • 客户端再操作槽位 0-5460 的 key 时,会自动路由到 node4

  • 如果原主节点 node1 后续重启,会自动变成 node4 的从节点,同步新主的数据;

  • 👉 大白话:业务无感知,读写请求正常路由到新主,集群恢复正常。

注意:集群内所有节点(包括主节点和从节点)都会收到这个 “node1 主观下线” 的 Gossip 包 —— 但只有主节点会参与「客观下线判定」和「投票选举」,从节点只做 “信息同步”,不参与决策。

Gossip包就是主和主、主和从、从和从,每个节点之间都建立tcp连接,每个节点都监听服务端口+10000的端口总线

主从复制是主和从之间通过服务端口进行

可以从以上学到,什么时候出现集群宕机

1:当主节点挂掉,却没有从节点进行故障转移

2:某个分片的主从全都挂掉

3:超过半数的master挂掉

无论是集群什么节点,一旦挂掉,都需要尽快的处理好

配置集群2.0

加入新的节点

  • 加主节点:redis-cli --cluster add-node 新节点IP:端口 任意旧节点IP:端口
  • 加从节点:redis-cli --cluster add-node 新节点IP:端口 任意旧节点IP:端口 \ --cluster-slave --cluster-master-id 主节点ID

这里注意,跟之前一样配置好node7和node8,然后docker启动起来

docker exec -it redis-node1 /bin/bash

redis-cli --cluster add-node 172.19.0.8:6379 172.19.0.2:6379

这里需要说明新增加的节点是谁:172.19.0.8:6379

然后后面跟集群即可,因为某个节点就代表了某个集群

这里先检查你指定的节点代表的集群的信息,以上检查说明没问题,然后向新节点发送 CLUSTER MEET 命令(让新节点加入集群),最终提示:新节点添加成功

能和集群其他节点通信,但不会处理任何业务请求(因为没有槽位,客户端路由时不会指向它);

# 进入任意旧主节点,执行槽位重分配
docker exec -it redis-node1 redis-cli --cluster reshard 172.19.0.2:6379
  • 输入要迁移的槽位数:比如 1000(从原主节点挪 1000 个槽位给新节点);
  • 输入新节点的 ID(先执行 docker exec redis-node7 redis-cli CLUSTER MYID 获取);
  • 输入 all(表示从所有主节点均匀迁移槽位);
  • 输入 yes 确认迁移。

//这一步是获取node7的ID
docker exec redis-node7 redis-cli CLUSTER MYID
//注意这一步需要把xxxx换成node7的ID
docker exec -it redis-node1 \
redis-cli --cluster add-node 172.19.0.9:6379 172.19.0.2:6379 \
--cluster-slave --cluster-master-id XXXXX

检查主从是否对

可以看到172.19.0.9是slave,它的master是172.19.0.8

也就是我们刚刚加入的主从节点

  • C++ 客户端只需配置任意一个集群节点的 6379 端口(比如 172.19.0.2:6379)作为 “初始连接点”;(这需要客户端具备路由功能)
  • 客户端连接后,会立即发送 CLUSTER NODES 命令,从该节点获取整个集群的节点拓扑、槽位映射表
  • 后续客户端会根据 key 的槽位,直接访问对应的节点(无需经过初始节点);
  • 若集群节点变化(如故障转移、槽位迁移),客户端会自动更新本地拓扑表,保持路由正确。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐