Redis 集群学到第 13 章已经把“主从复制”“哨兵机制”都过了一遍但说真的集群真正难的不是搭起来而是跑起来之后怎么应对故障以及客户端到底怎么正确接进来。很多人照着教程搭完集群一测主从切换就懵一写 Java 代码就报CLUSTERDOWN或者CROSSSLOT最后放弃了。这篇文章就把集群故障恢复、Java 连接集群的配置方式、以及集群本身的优缺点一次性讲透希望能帮你少走点弯路。不管你是正在学 Redis 的小白还是已经上手 Redis 但没深入集群的老手这部分内容都能直接照着用。1. 集群故障恢复机制解析1.1 故障是怎么被发现的集群不像哨兵那样在外部单独跑一套监控程序它自己就内置了故障检测能力。集群里的每个节点都会定时向其他节点发送PING消息如果某个节点在cluster-node-timeout默认 15000 毫秒时间内没有收到对方节点的PONG回复那么这个节点就会被标记为疑似下线也就是PFAIL。这里有个容易忽略的点疑似下线PFAIL是局部判断只有当前节点觉得你挂了真正让集群把某个节点判定为下线FAIL的需要多数持有槽的主节点都认为这个节点不可达。也就是说故障判定是“投票”出来的不是某个节点说了算的。这个设计的核心原因就是避免网络分区时出现误判——比如一个节点只是和另一个节点网络抖动并不代表它真的挂了通过多节点投票能大大降低误派的概率。我经常举一个类比这就像一个团队里有人经常迟到你不能因为一次迟到就觉得他离职了但如果团队里一半以上的人都连续一段时间联系不上他那基本可以确认这个人出事了。Redis 集群的PFAIL和FAIL就是这种从“疑似”到“确认”的过程。1.2 主从切换和故障转移流程一旦一个主节点被多数节点标记为FAIL并且这个主节点是某个或者多个槽的持有者集群就会触发故障转移failover。整个流程大概是从节点发现自己对应的主节点进入FAIL状态。从节点先检查自己是否具备参与选举的条件比如和主节点的最后通信时间不能太晚否则数据可能太旧不适合直接晋升。参与选举的从节点会向集群里其他持有槽的主节点发起投票请求获得超过半数主节点投票的从节点胜出。胜出的从节点会执行slaveof no one把自己提升为主节点同时把之前主节点负责的槽全部接管过来。集群会通过PONG消息广播这一变更其他节点更新路由信息。这个流程在数据一致性上其实是有舍取的。因为在PFAIL之前主节点可能已经收到了部分写命令但还没来得及复制给从节点这部分数据就会丢失。Redis 官方提供了一项wait指令可以缓解但绝大多数场景下集群模式接受有限的数据丢失来换取较高的可用性。如果你做的是金融级强一致需求的业务Redis 集群天然就不适合。实际操作中你可能会遇到主节点恢复了但它已经不是主了它重新加入集群后只能作为之前从节点的从节点。这个过程中集群会自动修改它的角色不需要手动干预。但是有一点要特别注意如果你之前手动改过某些节点的槽分配或者有人手滑执行了cluster failover之类命令恢复后的分片结构可能会和你预期不一致。所以每次做故障演练或者真实故障恢复之后一定要用redis-cli --cluster check再检查一遍集群状态。1.3 脑裂场景和数据丢失这里得单独提一下脑裂问题。所谓脑裂就是原来的主节点并没有真的掛只是它和集群其他节点之间的网络中断了同时它仍然在接收客户端的写请求。此时新选出来的主节点已经在服务了老主节点手里的数据在分区期间不断增长但网络恢复后老主节点发现自己的角色已经变成从节点它会尝试同步新主节点的数据这时老主节点上在分区期间写入的增量数据会被丢弃。这种情况下数据丢失不是 Redis 的 bug而是分布式系统在 CP 和 AP 之间做了选择的必然结果。Redis 集群选择的是可用性优先为了保证整体服务不中断必然牺牲强一致性。理解了这一点你就不会在故障恢复后纠结为什么有些数据突然消失了而是应该去判断业务是否能接受这种概率极小但确实存在的丢失。如果完全不接受那就别用集群或者上层做补偿机制。2. Java 开启集群模式客户端配置与实操2.1 Jedis 连接 Redis 集群的典型写法Java 生态里用得最多的客户端早期绝对是 Jedis。Jedis 连接集群的代码思路很直接创建一个JedisCluster对象传入一组初始节点的地址即可。import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisCluster; import java.util.HashSet; import java.util.Set; public class RedisClusterDemo { public static void main(String[] args) { SetHostAndPort nodes new HashSet(); // 不需要把所有节点都写上能连到一个节点就能自动发现整个集群拓扑 nodes.add(new HostAndPort(192.168.1.10, 7001)); nodes.add(new HostAndPort(192.168.1.11, 7002)); nodes.add(new HostAndPort(192.168.1.12, 7003)); JedisCluster jedisCluster new JedisCluster(nodes, 2000, 3000, 3); // 第一个参数是连接超时第二个是读写超时第三个是最大重定向次数 String result jedisCluster.set(hello:1, world); System.out.println(result); System.out.println(jedisCluster.get(hello:1)); // 用完之后别忘了关闭连接池 jedisCluster.close(); } }这段代码里最核心的是最后一个参数“最大重定向次数”。因为集群中的每个 key 根据哈希计算结果落在不同的槽上客户端第一次请求一个 key 时如果它连接的节点并不负责这个槽集群会返回一个MOVED重定向错误。Jedis 收到之后会重新发起请求到正确的节点这个重定向次数如果设置得太小比如 0那跨槽的请求就会直接报错。实际上在配置JedisCluster时官方建议用连接池而不是每次 new 一个实例。因为JedisCluster内部维护了每个节点的连接池如果你每次都创建新实例不仅会频繁进行 TCP 握手还会导致大量连接堆积压垮 Redis 服务器。2.2 基于 Lettuce 的集群配置方式现在新一代项目里Spring Boot 默认用的已经是 Lettuce 了。Lettuce 是异步和响应式驱动的底层是基于 Netty同一个连接可以在不同线程间复用所以它比 Jedis 更节省连接资源。如果你在用 Spring Boot最简单的方式是直接在配置类里建立一个RedisClusterConfigurationimport org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; Configuration public class RedisClusterConfig { Bean public RedisConnectionFactory redisConnectionFactory() { RedisClusterConfiguration configuration new RedisClusterConfiguration(); configuration.clusterNode(192.168.1.10, 7001); configuration.clusterNode(192.168.1.11, 7002); configuration.clusterNode(192.168.1.12, 7003); configuration.setMaxRedirects(3); configuration.setPassword(yourpassword); return new LettuceConnectionFactory(configuration); } }靠这一点配置Spring 的RedisTemplate就可以直接操作集群了。这里要注意Lettuce 的拓扑刷新机制值得说一说Lettuce 默认会定期从集群里获取最新的分片信息如果集群发生了主从切换或者槽迁移它能自动感知而不是像旧版本那样傻等重定向。但是拓扑刷新的频率并不是越高越好太频繁会产生额外的CLUSTER SLOTS命令压力。一般生产环境保持默认的 5 分钟即可如果需要更快感知故障可以适当调短。之前我遇到过一个问题集群里某个主节点挂了从节点晋升为一主但是 Java 程序还是持续往旧节点上写请求报错倒是没有但数据写不进去。其实这就是因为客户端拿到了旧拓扑没有及时刷新。处理办法就是在客户端配置里显式开启自适应拓扑刷新同时保证maxRedirects不要太小。对于 Lettuce 来说可以设置setTopologyRefreshOptions让它在连接断开时主动刷新拓扑。2.3 Java 客户端连接集群的常见报错和排查讲几个我真正踩过的坑每个都能让人卡半天。第一个CLUSTERDOWN The cluster is down。这个错误说明集群本身处于故障状态通常是集群里至少一个槽上没有可用节点。比如你给节点配置了cluster-enabled yes但地址写错了导致集群并没有完整创建或者主节点挂掉之后从节点没有顺利晋升。出现这个报错先别看 Java 代码直接用redis-cli -c进任意一个节点执行cluster info看cluster_state是不是ok。不是ok的话检查是不是有槽没被分配。第二个Moved或者Ask重定向错误。如果你用的是普通Jedis不是JedisCluster直接去访问集群就会得到这种错误。这没什么好说的Java 代码里不要用new Jedis()要使用JedisCluster或 Lettuce 的集群客户端。如果用了JedisCluster仍然报Moved多半是最大重定向次数设置成了 0。第三个CROSSSLOT Keys in request dont hash to the same slot。这是集群模式下经常犯的错因为集群把数据分散到了 16384 个槽里一个 key 只能落在一个槽中。对于mgetmset这类多 key 命令如果这些 key 不在同一个槽集群就无法支持。这种情况不能靠客户端解决要么在上层业务里按 key 分片后并发请求要么用 hash tag 强制让一组相关的 key 映射到同一个槽。比如把格式写成{user}.profile和{user}.orderRedis 只对大括号里的内容做哈希这样两个 key 就能落到同一个槽mget就不会报CROSSSLOT了。3. Redis 集群的优缺点到底值不值得上3.1 集群解决了什么问题Redis 集群最核心的价值就是“横向扩容”。单机 Redis 即使再快也有内存上限和单线程瓶颈一旦数据量超过单机内存或者并发读写超过单机处理能力单机再怎么优化都很难撑住。集群通过把数据分片到多台机器上让每台机器只负责一部分数据这样内存和数据吞吐都可以随节点增加而线性扩展。举个具体例子单台 Redis 8G 内存写入 QPS 大约 10 万当你数据量涨到 50G 时单机根本放不下。如果你搭建一个 10 节点集群每个节点只存 5G理论上总容量到 50G 甚至更高。更重要的是集群的读写压力被分散到了多个节点整个集群的吞吐上限会比单机高一个量级。另外集群模式天然支持主从复制和故障转移只要你的配置合理某个节点宕机后从节点可以自动顶上不需要像“主从 哨兵”那样额外维护一套哨兵组件。这也是为什么很多人跳过哨兵直接上集群。但要注意这不代表集群比哨兵更轻松相反集群的运维复杂度要高得多。3.2 集群的硬伤在哪里集群的缺点第一就是多 key 操作受限。前面提到的CROSSSLOT问题就是分布式数据分片带来的天然代价。哪些命令受影响mget、mset、del多 key 版本、sunion、sdiff、lpop多 key 推送、事务中操作多个不同槽的 key这些在集群模式下全都不可用。如果你业务里大量依赖这种跨键操作要么改代码要么提前设计好 key 的 hash tag。第二个缺点是数据迁移比较繁琐。当你要增加一个节点Redis 集群会将已有节点上的部分槽和对应的数据迁移到新节点。这个过程会占用网络 IO 和 CPU如果数据量很大迁移期间可能影响业务。槽迁移过程实际上是逐个槽迁移每个槽里可能有大量 keyredis-cli --cluster reshard会触发键迁移期间对这批 key 的访问可能性能下降甚至出现短暂超时。第三个缺点就是运维复杂度。你不能只监控一台机器的 CPU 和内存了要监控每个节点同时还要关注集群层面的槽分布、上下线状态、网络延迟。特别是节点之间交换PING/PONG消息如果节点数特别多网络风暴也是需要留意的。官方建议单集群节点数不要超过 1000 个这也是从网络开销角度出发的。3.3 什么场景适合用集群基于上面的权衡我觉得可以放心用集群的场景是这样的单机内存不足。比如你需要存 20G 以上缓存数据但单机内存加不到那么高或者加高成本太高。要求更高的写入吞吐。单机 Redis 写 QPS 已经接近瓶颈。能够接受极端情况下的小量数据丢失同时业务能在上层做补偿。反之如果业务对数据一致性要求极高或者大量使用多 key 操作、复杂事务或者其实单机完全扛得住那么使用集群反而是给自己找麻烦。Redis 本身单机性能已经够强没必要为了“用集群”而上集群。这里说一个我的实际感受生产上很多团队把缓存数据做了冷热分离后单机的热数据其实不大真正用上集群的往往是有几十 G 甚至上百 G 缓存且访问量比较均匀的场景。如果你只是小项目还是先把单机和持久化做好别一上来就整个集群否则光是日常维护就耗掉不少精力。4. 集群运维与故障恢复的实战排障4.1 典型的故障场景主节点宕机先模拟一个最常见的故障集群中的主节点node1:7001突然宕机它拥有一部分槽和一个从节点。正常情况下从节点会在cluster-node-timeout之后开始发起选举大约几秒到十几秒内完成切换。这个过程中你会看到业务侧出现超时或CLUSTERDOWN错误持续时间通常在秒级。从节点晋升为主节点后业务请求恢复。老的node1:7001重启后会自动变为集群中的从节点。这里我遇到过一种情况跟业务方说故障恢复很快业务侧报的错却持续了几分钟。后来排查发现是客户端没有做拓扑刷新一直往旧节点的地址发送请求。服务端确实已经恢复了但客户端回路没有更新。这类问题不取决于 Redis 本身而是取决于你怎么配置客户端。所以集群运维不能只看 Redis 侧还要确保客户端的拓扑感知能力开启并且要在业务代码里设置合理的重试机制。4.2 用命令手工检查集群健康状态在故障排查中常用命令一定要记熟。我最推荐的是:redis-cli --cluster check 任意节点IP:端口比如redis-cli --cluster check 192.168.1.10:7001这个命令会检查集群每个槽的分配情况打印每个节点的角色以及是否有主节点没有从节点。等输出完后最关心的其实是两个地方M:开头的是主节点S:开头的是从节点。要确认每个主节点都有对应的从节点不然这台主节点再挂就没得自动切换了。开头的[OK] All nodes agree about slots configuration说明槽分配一致。如果出现[WARNING] Node ... has slots in migrating state之类的提示说明有槽正在迁移。除了check还可以用redis-cli --cluster info查看整体数据分布情况。进入节点后用cluster nodes可以看到节点间的连接状态重点关注带disconnected字样的节点。4.3 手动触发故障切换演练比纸上谈兵重要我强烈建议你在测试环境做至少一次手动故障转移演练因为很多团队集群上线半年都没出过故障一旦真的出事了操作都不熟练。手动切换的命令在主节点上执行redis-cli -p 7001 cluster reset不对这个可不是用来切换的。要让某个从节点主动接管它的主节点在从节点上执行:redis-cli -p 7002 cluster failover这里有两种模式不带参数的是 graceful 切换它会尽量同步主节点上的增量数据等数据同步差不多后再发起切换如果主节点已经完全不可用从节点会通过常规选举自动晋升不需要手动执行命令。手动演练不只是为了看切换是否成功更重要的是感受整个切换过程中业务日志的报错情况。如果切换期间报错超过可接受范围就要调整cluster-node-timeout或者客户端超时配置。我建议你在演练的时候把 Redis 慢日志打开观察切换前后每条命令的耗时变化这能帮你判断是业务侧不够健壮导致的报错还是集群切换本身的锅。4.4 故障恢复后的数据校验故障恢复不等于一切结束。尤其是主节点宕机后老主节点如果重新上线它会作为从节点同步新主节点多出来的数据。为了确认数据确实没有出现大面积的分裂可以用redis-cli --cluster rebalance 任意节点IP:端口这个命令会触发槽重新均衡把槽和数据均匀分布到各个节点。不过要提醒一下虽然叫 rebalance它并不会在你没加节点的时候随意乱移数据只是会检查并尽量调整到均衡状态。如果集群状态本身正常它会提示无需操作。更好的校验方式是直接在业务里做一层对账比如缓存系统里每天定时从数据库取统计值和 Redis 里的缓存命中结果做比较。如果发现异常再针对性排查。5. 一些关于集群参数和客户端调优的经验5.1 cluster-node-timeout 不是越大越好很多人对cluster-node-timeout的理解是“故障超时时间设大一点稳妥一点”其实不然。它直接决定了节点判定故障的速度也就是从 PFAIL 到 FAIL 的时间基准。如果设得太大比如 60 秒那么主节点挂了之后业务会中断很长时间等从节点完成切换都已经超过一分钟了。如果设得太小比如 3 秒只要网络出现轻微抖动集群就可能频繁触发主从切换反而更不稳定。我生产环境一般设置在 10 到 15 秒之间同时把客户端侧的 socket 超时控制在 3 到 5 秒这样即使切换发生在单次请求上用户也基本不会感知到超时只知道偶发变慢。5.2 集群的从节点数量配置每个主节点至少配一个从节点这是底线。如果资源紧张最多保持每个主节点配一个从节点不要试图通过增加从节点数量来提高读性能。集群模式下读扩展一般是靠增加分片数而不是给某个主节点挂一堆从节点。因为 Redis 集群的读写路由都是基于槽的从节点只负责故障转移和高可用虽然也可以读从库但那样很容易读到过期数据而且会破坏一致性预期。5.3 监控指标不能只看 Redis一个健康的 Redis 集群光看 Redis 自身状态还不够节点机器的 CPU、内存、磁盘、网络流量都要纳入监控。磁盘其实是最容易被忽略的主从切换用了大量磁盘 IO 做 RDB 同步如果磁盘性能差同步就会慢主从切换期间数据延迟大极端情况下会触发从节点超时导致从节点重打全量同步。所以给集群节点挂 SSD 至少不是坏事条件允许的话别用机械盘顶着。个人的体会是Redis 集群虽然能解决容量和并发问题但它把更多运维责任转移给了使用者。如果你以前只用过单机 Redis刚开始上集群的那段时间最值得花的钱就是做一套全面的监控和告警不然故障发生时你会发现自己像个无头苍蝇。这篇文章基本上把我学习 Redis 集群过程中最重要的几个环节都过了一遍故障恢复的原理、Java 客户端的接入方式以及集群的优缺点和运维经验。最后送你一个小技巧每次修改集群配置或者做完故障演练记得把cluster nodes的输出和检查日志保存下来这样下次出问题时你能快速对比出集群状态到底变化了什么。
阅读完成 · 觉得有帮助?