☰
Redis集群实战总结:架构原理、部署与故障排查
2026/10/15 23:30:35 网站建设 项目流程

Redis 集群模式,我在生产环境里实际维护过三年。最早从一台单机 Redis 起步,缓存、Session、商品详情全塞在一台机器上,某天晚高峰内存直接冲到 90%,RDB 落盘的时候进程卡了好几秒,页面大面积超时。后来痛定思痛,把架构改成三主三从的 Redis 集群,容量、吞吐、故障切换都明显上一台阶。但集群也不是光靠一条命令搭起来就完事,槽位分配、迁移、故障转移、客户端兼容,每个环节都有坑。这篇文章就把我在实战里踩过的、排查过的东西完整梳理一遍,给正在评估或者已经在用 Redis 集群的人做个参考。

这篇文章适合谁看?一种是单机 Redis 已经顶不住,正在犹豫要不要上集群的;另一种是集群已经跑起来了,但遇到 slot 分配不均、扩容迁移失败、故障切换抖动这类问题,想找排查思路的。我会把集群的运行原理、部署步骤、扩容方法、高频问题排查都拆开讲,尽量用白话和真实案例把这件事讲透。

1. Redis集群到底解决了什么问题

1.1 单机Redis的三大瓶颈

Redis 单机再能打,也有物理上限。性能瓶颈主要体现在三块:内存、CPU 单线程、持久化阻塞。

内存是最直接的墙。一台物理机的内存是有限度的,就算你往 256G 的机器上怼,Redis 的数据集超过几十 G 之后,全量备份的时间、内存碎片整理的开销都会成倍增长。而且 Redis 的数据结构是内存密集型的,一个简单的字符串 key 加上元信息,实际占用的内存往往是数据本身的几倍。单机内存一旦达到容量上限,要么清数据,要么换机器,这都不是可持续的解法。

CPU 这块容易被忽略。Redis 核心处理是单线程的,命令在事件循环里排队逐条执行。虽然单线程避免了锁竞争,但面对高并发写入,CPU 单核在 100% 打满之后,命令响应就会明显变慢。我曾经遇到过一个场景,一批大 key 的 set 操作加批量读取同时打过来,单核 CPU 直接打满,连 ping 都延迟到几十毫秒。

持久化阻塞也隐蔽。默认 RDB 快照是 fork 子进程写盘的,fork 本身要拷贝页表,数据量越大 fork 耗时越长,期间主线程会短暂阻塞。在高峰期来一次全量 RDB,可能就是几秒的完全不可用。

1.2 主从加哨兵解决不了容量问题

很多团队最先接触的是主从复制加哨兵,也就是“一主多从 + 自动切换”。这套方案解决的是可用性问题:主节点挂了,哨兵能拉起从节点顶上。主从之间数据完全一致,从节点只分担读流量,但写流量和全部数据仍然集中在主节点上。

这里有个容易被误解的点:主从复制并没有提升系统容量。主节点存 10G 数据,每个从节点也存 10G 数据,内存总量翻了几倍,但逻辑容量还是 10G。写 QPS 也被主节点的单线程限制死死压住。

所以主从模式适合读多写少、数据总量可控的业务。但如果数据量已经超过了单机内存的合理水位,或者写 QPS 已经无法提升,这时候就必须考虑水平扩展。水平扩展的核心思路不是加从节点,而是把数据切开,分散到多个互不依赖的节点上,让每个节点只处理一部分数据。

1.3 集群模式的核心思路

Redis 集群模式把整个 key 空间分成了 16384 个哈希槽,每个节点负责其中一段槽区间。数据写入时,客户端对 key 做哈希计算,命中的槽归哪个节点管,请求就往哪个节点发。每个主节点可以配一个或多个从节点,主节点挂了,从节点顶上。

这套方案同时解决了三个问题:

  • 容量可扩展:数据分散在多个节点上,加节点就能增加总容量
  • 写性能可扩展:多个主节点并行处理写请求,绕开了单线程 CPU 的瓶颈
  • 高可用:每个分片都有从节点,单节点故障不会拖垮整个集群

集群模式不是没有代价。它要求客户端感知槽位和节点关系,跨槽位的多 key 操作受限,运维组件比单机复杂得多。这些代价是海量数据场景下不得不付出的,也是我们下面重点分析的内容。

2. 集群内部的工作机制:分片、复制与故障转移

2.1 16384个哈希槽是怎么算出来的

Redis 集群做数据分片用的不是一致性哈希,而是固定数量的哈希槽。计算规则很简单:对 key 做 CRC16 校验,得到的结果对 16384 取模。

[ slot = CRC16(key)\ %\ 16384 ]

举个例子,key 是user:10001,CRC16 运算得到一个十六进制数,假设是0x4BED,转成十进制后对 16384 取模,得到某个槽号。这个 key 最终只属于这个槽,不存在二义性。

集群创建的时候,管理员把 16384 个槽平均分配给所有主节点。三主集群就是每个节点分到大约 5461 个槽。写入的时候,节点会检查这个 key 的槽是否由自己负责,如果不是,就返回给客户端一个重定向信息,告诉客户端"这个 key 对应的节点是哪个"。

为什么选 16384 而不是更大数字?两个原因。首先,16384 个槽已经足够摊平数据了,再多没有意义,因为集群官方设计上限也就是 1000 个节点,16384 对 1000 个节点来说绰绰有余。其次,集群节点之间要定期交换状态信息,槽位信息会打包进心跳消息里,如果槽位数量太大,心跳包会变得很大,白白浪费网络带宽。16384 这个数字是经过权衡的,既保证槽位粒度够细,又不会让通信开销失控。

2.2 主从复制是怎么保持一致的

集群里每个主节点都带着一个或多个从节点,主从之间走的是 Redis 原生的复制协议。从节点启动后先做一次全量同步:主节点 fork 子进程生成 RDB 快照,传给从节点,从节点加载快照,再接收同步期间产生的新写命令。

全量同步之后,主从之间进入增量同步阶段。主节点会维护一个复制缓冲区,每条写命令除了执行,还会写进 backlog。从节点定时向主节点拉取新命令,靠的是复制偏移量来对齐。如果从节点断开时间过长,backlog 里的记录被覆盖了,那就只能重新做全量同步。

这里有个生产上需要特别注意的点:如果从节点和主节点之间网络经常抖动,导致频繁断线重连,可能每隔一段时间就触发一次全量同步,主节点就要反复 fork 出 RDB 传过去,这对主节点性能影响很大。Redis 的复制积压缓冲区大小由repl-backlog-size控制,默认 1MB,偏小。在写入量大的环境里,我通常调到 64MB 甚至 128MB,能有效减少全量重同步的概率。

2.3 槽迁移的完整过程

集群扩容、缩容、平衡数据,核心动作都是槽迁移。迁移不是把整个槽的数据拷过去那么简单,它要保证迁移过程中业务读写不中断。

假设要把槽 100 从节点 A 迁到节点 B,迁移过程大致是:节点 A 和节点 B 建立迁移关系,A 把槽 100 里的 key 分批取出来,发给 B,B 接收后写进自己的数据里,然后通知 A 删除这批 key。这个过程循环往复,直到 A 里的槽 100 数据为空,最后由管理员执行槽归属切换命令,才真正完成迁移。

迁移期间,客户端读这个槽的 key 时,可能还没迁走,A 直接返回;也可能已经在迁移路上了,A 会返回一个 ASK 重定向,告诉客户端去 B 节点读。这里区分一下两类重定向:

  • MOVED 重定向:槽的归属已经永久改变,客户端必须更新本地映射
  • ASK 重定向:槽正在迁移中,只是临时去向 B 请求一次,客户端不要更新本地映射

很多客户端开发第一次遇到 ASK 会很困惑,其实它就是一个"临时借道"的指令。迁移结束后,所有对这个槽的请求都会走 MOVED,路由信息自然就更新了。

2.4 故障转移是怎么判断和执行的

集群里每个节点都维持一个 cluster bus 通道,节点之间定时互相发心跳消息。当一个节点发现另一个节点心跳超时,会把这个节点标记为主观下线,也就是 PFAIL。这只是单个节点的判断,还没有形成共识。

随后节点会把这个状态广播出去,其他节点收到信息后也去尝试访问目标节点。当集群中持有槽的主节点里超过一半都认为目标节点下线时,目标节点被标记为客观下线(FAIL)。这时,目标节点的从节点会发起选举,争取成为新的主节点。

选举走的是类似 Raft 的机制:从节点向集群里其他持有槽的主节点请求投票,获得多数票之后,从节点提升为主节点,把原主节点负责的槽接管过来。这个切换过程是自动的,不需要人工介入。整个流程耗时和cluster-node-timeout有关系,默认 15 秒,我生产上一般会调到 5 秒左右,切换更快,但也要小心网络抖动导致误判。

这里有个关键点:故障转移是主节点挂掉后从节点顶上,但如果整个分片的主从都挂了,这个分片的数据就永久不可用了。所以生产上不要只配置一主一从,至少保证每个分片有一份完整的冗余。

3. 三主三从集群部署实操

3.1 准备阶段:配置文件里的几个关键参数

部署集群前先把基础规划做好。我常用的方案是 6 个节点,3 主 3 从,每个主节点 1 个从节点。这在生产上是最小的高可用配置,既能抗单节点故障,又能做故障转移测试。

每个节点都是独立的 Redis 进程,可以部署在同一台机器上模拟,也可以分布在多台物理机上。生产环境当然要分散,避免一台机器宕机损失多个分片。下面是一份标准的集群节点配置文件:

port 7000 cluster-enabled yes cluster-config-file nodes-7000.conf cluster-node-timeout 5000 appendonly yes appendfilename "appendonly-7000.aof" maxmemory 8gb maxmemory-policy allkeys-lru

重点解释几个参数:

  • cluster-enabled yes:开启集群模式,没有这个配置,节点就是一个普通 Redis 实例
  • cluster-node-timeout 5000:节点心跳超时阈值,设置太大会拖慢故障转移,设置太小容易在网络抖动时误判
  • appendonly yes:开启 AOF 持久化,集群节点最好开启,避免宕机重启后数据大量丢失
  • maxmemory-policy allkeys-lru:内存淘汰策略,缓存型业务一般用 allkeys-lru,保证内存不满时不至于 OOM

每个节点端口不同,配置文件也不同。我把 6 个节点放在 7000 到 7005 端口,先用本地环境把流程跑通。

3.2 创建集群的具体步骤

准备完配置文件后,依次启动 6 个 Redis 实例:

redis-server /etc/redis/7000.conf redis-server /etc/redis/7001.conf # 其他节点同理

实例启动后,节点之间还没有建立联系,需要用命令把节点组织成一个集群:

redis-cli --cluster create \ 127.0.0.1:7000 \ 127.0.0.1:7001 \ 127.0.0.1:7002 \ 127.0.0.1:7003 \ 127.0.0.1:7004 \ 127.0.0.1:7005 \ --cluster-replicas 1

--cluster-replicas 1表示每个主节点配置 1 个从节点。这个命令会自动完成两件事:先把 16384 个槽平均分给前三个节点,再为每个主节点分配一个从节点。执行成功后,输出会显示每个主节点的槽位区间和从节点对应关系。

这时候用redis-cli --cluster check 127.0.0.1:7000检查集群状态,输出里会列出每个节点负责的槽位数,以及集群的总体状态。正常情况应该是 0 个手动 failover,所有槽都已分配,集群状态是 ok,没有处于迁移中的槽。

3.3 读写测试与客户端连接注意事项

集群创建完之后,用 redis-cli 测试写读:

redis-cli -c -p 7000 127.0.0.1:7000> set name zhangsan -> Redirected to slot [5798] located at 127.0.0.1:7001 OK 127.0.0.1:7001> get name "zhangsan"

注意-c参数,这是集群模式开关。如果不加-c,连上 7000 端口后执行 set 命令,如果 key 对应的槽不在 7000 节点上,会直接返回 MOVED 错误。加了-c之后,redis-cli 会自动跟随重定向跳转到正确的节点。

生产环境的业务代码不直接连单个端口,而是通过客户端库连接集群。以 Java 的 Jedis 为例,初始化时需要传入多个节点地址:

Set<HostAndPort> nodes = new HashSet<>(); nodes.add(new HostAndPort("10.0.0.1", 7000)); nodes.add(new HostAndPort("10.0.0.2", 7001)); nodes.add(new HostAndPort("10.0.0.3", 7002)); JedisCluster jedisCluster = new JedisCluster(nodes);

客户端初始化时会自动拉取集群拓扑,之后会根据 key 的哈希槽直接路由到对应节点,不需要每次走重定向。这一点和单体 Redis 的连接方式差别很大,很多第一次接触集群的同事都会在这里搞混,直接用单机客户端连集群,结果自然是各种报错。

还有一个容易踩的坑是多 key 操作。比如在单机 Redis 里执行mget key1 key2 key3很常见,但在集群模式下,如果这三个 key 的哈希槽不在同一个节点上,MGET 会直接失败。解决办法是利用哈希标签,让相关的 key 共享同一个槽。比如把 key 写成user:{1001}:profile和user:{1001}:orders,花括号里的内容参与哈希计算,这两个 key 就会被分到同一个槽,MGET 就能用。这是集群模式下客户端改造绕不开的一个点。

4. 集群运维与容量规划中的关键细节

4.1 每个节点规划多少内存

集群容量不是把所有节点的内存简单相加。每个主节点的内存规划,要按实际可用内存的 60% 到 70% 来估算,预留一部分给 Redis 自身的缓冲、内存碎片和 RDB fork 时的页表复制开销。

假设你有一台 16G 内存的服务器,分配给 Redis 进程的 maxmemory 建议不超过 10G,实际可用容量估算在 6G 到 7G 比较稳。它是 3 主节点的集群,总逻辑容量大约就是 18G 到 21G,数据量超过这个水位就需要考虑扩节点了。

这里有个实际案例:我维护过一套集群,每个主节点 maxmemory 设成 12G,数据量涨到 10G 的时候,内存碎片率已经到了 1.5,再加 RDB fork 的瞬时内存占用,节点直接 OOM 被内核杀掉。后来把数据清理到 8G 以下,并把 maxmemory 调低,才稳定下来。内存规划宁可用不满,也不能让它逼近上限。

4.2 热点 key 和内存倾斜问题

集群的数据分布是按哈希槽均匀分的,但业务数据不一定是均匀的。一个典型的场景是:某几个 key 的访问量特别大,占了整个集群访问量的 80%,这几个 key 所在的节点负载明显高于其他节点,这就叫热点 key 导致的内存和流量倾斜。

热点 key 的解决办法通常是加一层本地缓存。对于热点数据,在业务进程里维护一个小容量的本地缓存,几千个请求只有第一个会打到 Redis,其余直接命中本地缓存,Redis 节点压力瞬间就下来了。不要指望集群能自动解决热点问题,集群只管分片,不管访问均衡和热点迁移。

哈希槽分布不均也会导致节点之间数据量差异很大。用redis-cli --cluster rebalance可以自动调整槽位分布,但它会触发大规模的数据迁移,生产环境执行前要评估迁移窗口和业务影响。

4.3 备份与恢复的正确姿势

集群模式下没有一条命令能备份所有节点。很多人沿用单机的思路,在任意一个节点上执行 BGSAVE,发现只备份了一个节点的数据,其他节点完全没反应。这其实就是集群和单机在运维上的最大区别。

正确的备份方式是:在每个节点上分别执行 BGSAVE,或者直接备份每个节点的 AOF 文件和 RDB 文件。恢复时也要逐个节点恢复,然后重新组建集群。如果只是某个分片的主节点数据丢失,可以从它的从节点拷贝 RDB 文件做恢复,但这个从节点在恢复期间要保证不接收新的写请求,否则会覆盖恢复进去的数据。

备份要设周期任务,我一般每天凌晨对每个节点执行一次 BGSAVE,然后拷贝 RDB 文件到独立的备份存储。恢复演练也要做一次,不然真到出事的时候,你会发现备份文件拷回来根本起不来。备份文件损坏和版本不兼容都是真实遇到过的问题。

5. 实际运维中高频问题与排查实录

5.1 cluster_state:fail 的几种可能原因

集群状态变成 fail 的时候,日志或者监控上会看到Cluster state changed: ok -> fail。这个状态表示集群无法提供完整服务了。常见原因有三个。

第一种,某个主节点失联,导致它负责的槽位没有可用的主节点,集群就无法覆盖完整槽位,立即进入 fail 状态。这种情况通常是某个分片的主从同时挂了,检查对应机器的进程和网络。

第二种,集群节点之间网络闪断,心跳包丢了几秒。如果cluster-node-timeout设置过小,比如我调试时设过 1 秒,网络一抖动就把正常节点误判为下线,触发大量故障转移,集群卡在迁移中。生产环境建议 5 到 10 秒,不能为了追求快速切换把阈值调到太小。

第三种,槽位分布不完整。比如手动操作槽位迁移失败,或者新节点加入后没有分配槽位,cluster_info里的cluster_slots_ok小于 16384,集群就会拒绝服务。用redis-cli --cluster fix可以尝试修复槽位分配问题。

5.2 MOVED 和 ASK 重定向的排查思路

业务侧偶尔会看到类似MOVED 7983 127.0.0.1:7001或者ASK 7983 127.0.0.1:7001的错误日志。这条日志其实是正常现象,说明客户端拿到了重定向指令。

但如果业务侧频繁出现这种错误,可能就不是正常重定向,而是客户端没有正确更新路由表,每次都要请求错误节点再被重定向回来,性能自然下降。排查方向如下:

  • 确认客户端库版本是否支持集群模式
  • 确认初始化的节点列表是否包含了正确的集群节点
  • 确认使用期间是否发生过扩容缩容,客户端是否需要重新初始化

ASK 频繁出现则说明当时正在做槽迁移。如果迁移任务已经结束,但 ASK 还出现,可能是迁移收尾不干净,用cluster setslot手动确认槽位归属。

5.3 扩容时迁移卡住怎么处理

扩容集群一般就两步:把新节点加入集群,然后从旧节点迁移部分槽位给新节点。

# 把新节点加入集群 redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 # 迁移槽位 redis-cli --cluster reshard 127.0.0.1:7000 \ --cluster-from <旧节点ID> \ --cluster-to <新节点ID> \ --cluster-slots 800 \ --cluster-yes

迁移卡住最典型的原因是单个 key 太大。集群迁移槽位时是按 key 逐个迁移的,如果一个 key 达到几百 MB,迁移单个 key 就要很长时间,整个迁移任务看起来就像卡住了一样。排查时用redis-cli --cluster reshard的输出观察,如果长时间停留在同一个 key 上,基本就是大 key 问题。

另一个原因是目标节点的内存不够,迁移过去的数据超过 maxmemory 上限,写不进去。加节点前一定先评估目标节点的内存水位,别加完节点才发现容量不够。

5.4 不容忽视的脑裂和数据丢失

集群模式的主从复制依然是异步的。主节点接收写请求后,先返回给客户端成功,再异步复制给从节点。如果主节点现场发生了网络分区,从节点被选举成新主节点,期间旧主节点接到的所有写请求都会丢失。

更麻烦的是,旧主节点恢复后发现自己已经变成从节点,会自动向新主节点发起全量同步,用自己的数据覆盖已经是空的。这就等于把分区期间的写入彻底抹掉了。

缓解脑裂的策略有两个维度。一是配置层面的min-replicas-to-write和min-replicas-max-lag,让主节点在从节点数量不足或者数据落后太严重时拒绝写入,但这会牺牲可用性。另一个是业务层面做数据兜底,对一致性要求高的写入,除了 Redis 还要持久化到数据库,Redis 只做缓存。不要试图在一个异步复制的系统里追求强一致,那只会让你在事故复盘时更加被动。

5.5 误操作 flushall 的恢复教训

最后说一个看起来很蠢但我真的在测试环境遇到过的问题:有人连上某个节点执行了 flushall。在集群模式下,这条命令只清空当前连接节点的本地数据,不会广播到其他节点。

后果就是:这个节点负责的那些槽,数据全没了。其他节点的数据还在,但整个集群的数据变成了残缺状态。

恢复方案有两种。如果这个节点有从节点,立即把从节点提升为主节点,也就是做一次手动 failover,这样数据完整回到主节点上。如果没配从节点,就只能从最近的备份恢复,并且需要接受备份时间点之后的数据丢失。

所以生产环境千万不要在任意节点上随意执行 flushall、flushdb 这类命令。即使是测试环境,也建议单独开集群实验,别和生产集群混在一起。要清理数据时,通过控制台或者带审批的自动化工具执行,并且逐节点确认。

写在后面:我的一点个人体会

集群模式不是银弹,它解决的是容量和写性能的扩展问题,代价是运维复杂度明显上升。如果数据量还在单机可以搞定的范围,主从加哨兵反而是更简单稳妥的架构。我这三年里看到不少团队在数据量几百 G 的时候就强行上集群,结果真正的问题不是容量,而是跨 key 事务、多键操作和客户端改造带来的成本,最后又退回单机加缓存分片的做法。所以架构选型先算清楚账:你的数据量到底多大,写 QPS 到底多高,有没有强事务需求,这些想清楚再决定要不要集群。真确定要用了,建议先把本文第三部分的部署流程在测试环境完整跑一遍,再考虑生产改造,尤其要把扩容和故障转移演练做扎实。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询