Redis 集群方案详解:主从复制、哨兵、脑裂、分片集群和哈希槽
单节点 Redis 再快也会遇到三个问题单节点并发能力有上限。单节点宕机会导致服务不可用。单节点内存有限无法承载海量数据。Redis 的集群方案就是围绕这三个问题展开的主从复制解决读扩展哨兵解决自动故障恢复分片集群解决海量数据和高并发写。一、主从复制读写分离主从复制通常是一主多从。主节点负责写从节点负责读从而提高整体并发能力。主从复制适合读多写少的场景。但它只解决读扩展不解决 Master 宕机后的自动切换问题。二、主从同步全量同步和增量同步第一次同步通常是全量同步如果从节点短暂断开后重连Master 会根据 offset 从repl_backlog中找到断开期间的命令做增量同步。三、哨兵自动故障恢复Sentinel 哨兵机制用于主从集群的自动故障恢复。它主要做三件事功能说明监控定期 ping 主从节点检查服务状态自动故障恢复Master 宕机后从 Slave 中选举新 Master通知通知客户端新的 Master 地址哨兵通过心跳判断节点状态状态含义主观下线某个 Sentinel 认为节点不可用客观下线超过 quorum 数量的 Sentinel 都认为节点不可用quorum 最好超过 Sentinel 实例数量的一半。四、脑裂为什么会出现两个 Master脑裂通常发生在网络分区时。老 Master 和 Sentinel 失去联系Sentinel 认为 Master 下线于是把某个 Slave 提升为新 Master。但客户端可能仍然能连接老 Master并继续写入数据。课件里给了两个配置来降低脑裂风险min-replicas-to-write 1min-replicas-max-lag 5含义是至少要有 1 个从节点并且复制延迟不能超过 5 秒否则 Master 拒绝写入。这样老 Master 如果和从节点失联就不会继续接收写请求。五、分片集群解决海量数据和高并发写主从和哨兵解决了高可用和高并发读但没有解决两个问题海量数据存储。高并发写。分片集群中会有多个 Master每个 Master 保存不同的数据每个 Master 又可以有多个 Slave。客户端可以访问集群任意节点最终请求会被转发到正确节点。六、哈希槽key 到底存在哪个节点Redis Cluster 引入了哈希槽。整个集群有 16384 个槽每个 key 通过 CRC16 校验后对 16384 取模得到槽位。slot CRC16(key) % 16384每个 Master 负责一部分槽。例如节点负责槽位Master 10 - 5460Master 25461 - 10922Master 310923 - 16383如果 key 中有大括号Redis 会使用大括号里的内容作为有效部分计算槽位user:{1001}:name order:{1001}:list这样可以让相关 key 落到同一个槽便于执行某些多 key 操作。七、深入实践Java 客户端连接与代码示例理解了 Redis 集群的原理后我们来看看如何在 Java 应用中实际使用这些集群方案。这里以 Jedis 和 Lettuce 两个主流客户端为例。7.1 连接主从哨兵集群当使用主从哨兵方案时客户端需要连接哨兵节点来获取当前可用的 Master 地址。使用 Jedis 连接哨兵集群importredis.clients.jedis.Jedis;importredis.clients.jedis.JedisSentinelPool;importjava.util.HashSet;importjava.util.Set;publicclassSentinelExample{publicstaticvoidmain(String[]args){// 1. 配置哨兵节点SetStringsentinelsnewHashSet();sentinels.add(192.168.1.101:26379);sentinels.add(192.168.1.102:26379);sentinels.add(192.168.1.103:26379);// 2. 主节点名称在哨兵配置中定义的 master nameStringmasterNamemymaster;// 3. 创建哨兵连接池JedisSentinelPoolpoolnewJedisSentinelPool(masterName,sentinels);try(Jedisjedispool.getResource()){// 4. 执行操作会自动连接到当前 Masterjedis.set(key1,value1);Stringvaluejedis.get(key1);System.out.println(获取的值: value);// 5. 模拟故障转移// 当 Master 宕机时哨兵会选举新的 Master// JedisSentinelPool 会自动检测并切换到新的 Master}finally{pool.close();}}}使用 Lettuce 连接哨兵集群importio.lettuce.core.RedisClient;importio.lettuce.core.RedisURI;importio.lettuce.core.api.StatefulRedisConnection;importio.lettuce.core.api.sync.RedisCommands;importio.lettuce.core.sentinel.api.StatefulRedisSentinelConnection;importio.lettuce.core.sentinel.api.sync.RedisSentinelCommands;publicclassLettuceSentinelExample{publicstaticvoidmain(String[]args){// 1. 构建哨兵连接 URIRedisURIsentinelUriRedisURI.Builder.sentinel(192.168.1.101,26379,mymaster).withSentinel(192.168.1.102,26379).withSentinel(192.168.1.103,26379).build();// 2. 创建客户端RedisClientclientRedisClient.create(sentinelUri);try(StatefulRedisConnectionString,Stringconnectionclient.connect()){RedisCommandsString,Stringcommandsconnection.sync();// 3. 执行操作commands.set(counter,100);Stringresultcommands.get(counter);System.out.println(计数器值: result);// 4. 发布订阅示例哨兵模式下commands.publish(channel:updates,系统状态更新);}finally{client.shutdown();}}}7.2 连接 Redis Cluster 分片集群对于分片集群客户端需要知道所有节点地址并自动处理哈希槽的映射和重定向。使用 Jedis 连接 Redis Clusterimportredis.clients.jedis.HostAndPort;importredis.clients.jedis.JedisCluster;importredis.clients.jedis.JedisPoolConfig;importjava.util.HashSet;importjava.util.Set;publicclassClusterExample{publicstaticvoidmain(String[]args){// 1. 配置连接池JedisPoolConfigpoolConfignewJedisPoolConfig();poolConfig.setMaxTotal(100);poolConfig.setMaxIdle(20);poolConfig.setMinIdle(5);// 2. 添加集群节点至少一个客户端会自动发现其他节点SetHostAndPortnodesnewHashSet();nodes.add(newHostAndPort(192.168.1.101,7001));nodes.add(newHostAndPort(192.168.1.102,7002));nodes.add(newHostAndPort(192.168.1.103,7003));// 3. 创建集群客户端try(JedisClusterjedisClusternewJedisCluster(nodes,poolConfig)){// 4. 基本操作自动路由到正确的节点jedisCluster.set(user:1001:name,张三);jedisCluster.set(user:1001:age,25);// 5. 使用哈希标签确保相关 key 在同一个槽jedisCluster.set({user:1001}:profile,{\city\:\北京\,\job\:\工程师\});jedisCluster.set({user:1001}:settings,{\theme\:\dark\,\lang\:\zh\});// 6. 批量操作相同哈希标签的 key 可以批量操作StringprofilejedisCluster.get({user:1001}:profile);System.out.println(用户资料: profile);// 7. 集群信息查询System.out.println(集群槽位分布: jedisCluster.clusterSlots());}catch(Exceptione){e.printStackTrace();}}}使用 Lettuce 连接 Redis Clusterimportio.lettuce.core.RedisURI;importio.lettuce.core.cluster.RedisClusterClient;importio.lettuce.core.cluster.api.StatefulRedisClusterConnection;importio.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;importjava.util.Arrays;importjava.util.List;publicclassLettuceClusterExample{publicstaticvoidmain(String[]args){// 1. 构建集群节点 URIListRedisURInodesArrays.asList(RedisURI.create(redis://192.168.1.101:7001),RedisURI.create(redis://192.168.1.102:7002),RedisURI.create(redis://192.168.1.103:7003));// 2. 创建集群客户端RedisClusterClientclusterClientRedisClusterClient.create(nodes);try(StatefulRedisClusterConnectionString,StringconnectionclusterClient.connect()){RedisAdvancedClusterCommandsString,Stringcommandsconnection.sync();// 3. 执行跨节点操作commands.set(product:1001:name,iPhone 15);commands.set(product:1001:price,6999);// 4. 管道操作示例提升批量操作性能commands.setAutoFlushCommands(false);commands.set(order:2024001,pending);commands.incr(global:order:count);commands.expire(order:2024001,3600);commands.flushCommands();// 一次性发送// 5. 事务操作相同哈希槽的 keycommands.multi();commands.set({account:1001}:balance,5000);commands.decrBy({account:1001}:balance,1000);commands.exec();// 6. 集群状态监控System.out.println(集群节点信息:);commands.clusterNodes().forEach(System.out::println);}finally{clusterClient.shutdown();}}}7.3 生产环境最佳实践连接池配置优化JedisPoolConfigconfignewJedisPoolConfig();config.setMaxTotal(200);// 最大连接数config.setMaxIdle(50);// 最大空闲连接config.setMinIdle(10);// 最小空闲连接config.setMaxWaitMillis(5000);// 获取连接最大等待时间config.setTestOnBorrow(true);// 借出时测试连接config.setTestWhileIdle(true);// 空闲时测试连接重试机制与故障处理publicclassResilientRedisClient{privatestaticfinalintMAX_RETRIES3;publicStringgetWithRetry(JedisClustercluster,Stringkey){intretries0;while(retriesMAX_RETRIES){try{returncluster.get(key);}catch(Exceptione){retries;if(retriesMAX_RETRIES){thrownewRuntimeException(获取数据失败重试次数耗尽,e);}try{Thread.sleep(100*retries);// 指数退避}catch(InterruptedExceptionie){Thread.currentThread().interrupt();}}}returnnull;}}监控与告警集成// 使用 Micrometer 或类似框架监控 Redis 指标publicclassRedisMetrics{privatefinalMeterRegistryregistry;privatefinalJedisClustercluster;publicvoidrecordOperation(Stringoperation,longduration){Timer.builder(redis.operation.duration).tag(operation,operation).register(registry).record(duration,TimeUnit.MILLISECONDS);}publicvoidcheckClusterHealth(){try{StringclusterInfocluster.clusterInfo();// 解析集群状态触发告警if(clusterInfo.contains(cluster_state:fail)){sendAlert(Redis 集群状态异常);}}catch(Exceptione){sendAlert(Redis 集群健康检查失败: e.getMessage());}}}7.4 性能对比与选型建议客户端优点缺点适用场景Jedis1. API 简单直观2. 社区成熟文档丰富3. 同步阻塞式符合传统习惯1. 连接非线程安全2. 高并发需要连接池3. 集群模式下重连逻辑较简单传统 Spring 项目中小规模并发Lettuce1. 基于 Netty性能更高2. 支持异步/响应式编程3. 连接线程安全4. 自动重连和拓扑刷新1. 学习曲线稍陡2. 内存占用相对较高高并发微服务Spring Boot 2.x需要响应式支持Redisson1. 分布式对象和服务丰富2. 支持分布式锁、队列等3. 与 Java 数据结构无缝集成1. 功能复杂较重2. 配置相对繁琐需要高级分布式功能如分布式锁、延迟队列等选型建议新项目推荐使用Lettuce特别是 Spring Boot 2.x 以上版本老项目维护或简单场景可用Jedis需要复杂分布式数据结构时考虑Redisson七、怎么选集群方案场景推荐方案小型项目Redis 压力不大单机或一主一从需要自动故障恢复主从 哨兵数据量大写压力高Redis Cluster 分片集群单节点内存超过 10G优先考虑拆分服务或分片面试可以这样说Redis 集群方案主要有主从复制、哨兵和分片集群。主从复制通过读写分离提升读并发哨兵在主从基础上增加监控、自动故障转移和通知能力分片集群通过多个 Master 分摊数据和写压力并通过 16384 个哈希槽决定 key 的存储位置。脑裂可以通过限制最少从节点数量和最大复制延迟来降低风险。