Redis 集群方案详解:主从复制、哨兵、脑裂、分片集群和哈希槽

单节点 Redis 再快,也会遇到三个问题:

  1. 单节点并发能力有上限
  2. 单节点宕机会导致服务不可用
  3. 单节点内存有限,无法承载海量数据

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 1
  • min-replicas-max-lag 5

含义是:至少要有 1 个从节点,并且复制延迟不能超过 5 秒,否则 Master 拒绝写入。这样老 Master 如果和从节点失联,就不会继续接收写请求。

五、分片集群:解决海量数据和高并发写

主从和哨兵解决了高可用和高并发读,但没有解决两个问题:

  1. 海量数据存储
  2. 高并发写

分片集群中会有多个 Master,每个 Master 保存不同的数据,每个 Master 又可以有多个 Slave。

客户端可以访问集群任意节点,最终请求会被转发到正确节点。

六、哈希槽:key 到底存在哪个节点?

Redis Cluster 引入了哈希槽。整个集群有 16384 个槽,每个 key 通过 CRC16 校验后对 16384 取模,得到槽位。

复制代码
slot = CRC16(key) % 16384

每个 Master 负责一部分槽。例如:

节点 负责槽位
Master 1 0 - 5460
Master 2 5461 - 10922
Master 3 10923 - 16383

如果 key 中有大括号,Redis 会使用大括号里的内容作为有效部分计算槽位:

复制代码
user:{1001}:name
order:{1001}:list

这样可以让相关 key 落到同一个槽,便于执行某些多 key 操作。

七、深入实践:Java 客户端连接与代码示例

理解了 Redis 集群的原理后,我们来看看如何在 Java 应用中实际使用这些集群方案。这里以 Jedis 和 Lettuce 两个主流客户端为例。

7.1 连接主从+哨兵集群

当使用主从+哨兵方案时,客户端需要连接哨兵节点来获取当前可用的 Master 地址。

使用 Jedis 连接哨兵集群:

java 复制代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;
import java.util.HashSet;
import java.util.Set;

public class SentinelExample {
    public static void main(String[] args) {
        // 1. 配置哨兵节点
        Set<String> sentinels = new HashSet<>();
        sentinels.add("192.168.1.101:26379");
        sentinels.add("192.168.1.102:26379");
        sentinels.add("192.168.1.103:26379");
        
        // 2. 主节点名称(在哨兵配置中定义的 master name)
        String masterName = "mymaster";
        
        // 3. 创建哨兵连接池
        JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels);
        
        try (Jedis jedis = pool.getResource()) {
            // 4. 执行操作(会自动连接到当前 Master)
            jedis.set("key1", "value1");
            String value = jedis.get("key1");
            System.out.println("获取的值: " + value);
            
            // 5. 模拟故障转移
            // 当 Master 宕机时,哨兵会选举新的 Master
            // JedisSentinelPool 会自动检测并切换到新的 Master
        } finally {
            pool.close();
        }
    }
}

使用 Lettuce 连接哨兵集群:

java 复制代码
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection;
import io.lettuce.core.sentinel.api.sync.RedisSentinelCommands;

public class LettuceSentinelExample {
    public static void main(String[] args) {
        // 1. 构建哨兵连接 URI
        RedisURI sentinelUri = RedisURI.Builder
                .sentinel("192.168.1.101", 26379, "mymaster")
                .withSentinel("192.168.1.102", 26379)
                .withSentinel("192.168.1.103", 26379)
                .build();
        
        // 2. 创建客户端
        RedisClient client = RedisClient.create(sentinelUri);
        
        try (StatefulRedisConnection<String, String> connection = client.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            
            // 3. 执行操作
            commands.set("counter", "100");
            String result = commands.get("counter");
            System.out.println("计数器值: " + result);
            
            // 4. 发布订阅示例(哨兵模式下)
            commands.publish("channel:updates", "系统状态更新");
        } finally {
            client.shutdown();
        }
    }
}

7.2 连接 Redis Cluster 分片集群

对于分片集群,客户端需要知道所有节点地址,并自动处理哈希槽的映射和重定向。

使用 Jedis 连接 Redis Cluster:

java 复制代码
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;
import java.util.HashSet;
import java.util.Set;

public class ClusterExample {
    public static void main(String[] args) {
        // 1. 配置连接池
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(100);
        poolConfig.setMaxIdle(20);
        poolConfig.setMinIdle(5);
        
        // 2. 添加集群节点(至少一个,客户端会自动发现其他节点)
        Set<HostAndPort> nodes = new HashSet<>();
        nodes.add(new HostAndPort("192.168.1.101", 7001));
        nodes.add(new HostAndPort("192.168.1.102", 7002));
        nodes.add(new HostAndPort("192.168.1.103", 7003));
        
        // 3. 创建集群客户端
        try (JedisCluster jedisCluster = new JedisCluster(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 可以批量操作)
            String profile = jedisCluster.get("{user:1001}:profile");
            System.out.println("用户资料: " + profile);
            
            // 7. 集群信息查询
            System.out.println("集群槽位分布: " + jedisCluster.clusterSlots());
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用 Lettuce 连接 Redis Cluster:

java 复制代码
import io.lettuce.core.RedisURI;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import java.util.Arrays;
import java.util.List;

public class LettuceClusterExample {
    public static void main(String[] args) {
        // 1. 构建集群节点 URI
        List<RedisURI> nodes = Arrays.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. 创建集群客户端
        RedisClusterClient clusterClient = RedisClusterClient.create(nodes);
        
        try (StatefulRedisClusterConnection<String, String> connection = 
                clusterClient.connect()) {
            
            RedisAdvancedClusterCommands<String, String> commands = connection.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. 事务操作(相同哈希槽的 key)
            commands.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 生产环境最佳实践

  1. 连接池配置优化
java 复制代码
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200);          // 最大连接数
config.setMaxIdle(50);            // 最大空闲连接
config.setMinIdle(10);            // 最小空闲连接
config.setMaxWaitMillis(5000);    // 获取连接最大等待时间
config.setTestOnBorrow(true);     // 借出时测试连接
config.setTestWhileIdle(true);    // 空闲时测试连接
  1. 重试机制与故障处理
java 复制代码
public class ResilientRedisClient {
    private static final int MAX_RETRIES = 3;
    
    public String getWithRetry(JedisCluster cluster, String key) {
        int retries = 0;
        while (retries < MAX_RETRIES) {
            try {
                return cluster.get(key);
            } catch (Exception e) {
                retries++;
                if (retries == MAX_RETRIES) {
                    throw new RuntimeException("获取数据失败,重试次数耗尽", e);
                }
                try {
                    Thread.sleep(100 * retries); // 指数退避
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }
        return null;
    }
}
  1. 监控与告警集成
java 复制代码
// 使用 Micrometer 或类似框架监控 Redis 指标
public class RedisMetrics {
    private final MeterRegistry registry;
    private final JedisCluster cluster;
    
    public void recordOperation(String operation, long duration) {
        Timer.builder("redis.operation.duration")
            .tag("operation", operation)
            .register(registry)
            .record(duration, TimeUnit.MILLISECONDS);
    }
    
    public void checkClusterHealth() {
        try {
            String clusterInfo = cluster.clusterInfo();
            // 解析集群状态,触发告警
            if (clusterInfo.contains("cluster_state:fail")) {
                sendAlert("Redis 集群状态异常");
            }
        } catch (Exception e) {
            sendAlert("Redis 集群健康检查失败: " + e.getMessage());
        }
    }
}

7.4 性能对比与选型建议

客户端 优点 缺点 适用场景
Jedis 1. API 简单直观 2. 社区成熟,文档丰富 3. 同步阻塞式,符合传统习惯 1. 连接非线程安全 2. 高并发需要连接池 3. 集群模式下重连逻辑较简单 传统 Spring 项目,中小规模并发
Lettuce 1. 基于 Netty,性能更高 2. 支持异步/响应式编程 3. 连接线程安全 4. 自动重连和拓扑刷新 1. 学习曲线稍陡 2. 内存占用相对较高 高并发微服务,Spring Boot 2.x+,需要响应式支持
Redisson 1. 分布式对象和服务丰富 2. 支持分布式锁、队列等 3. 与 Java 数据结构无缝集成 1. 功能复杂,较重 2. 配置相对繁琐 需要高级分布式功能,如分布式锁、延迟队列等

选型建议:

  • 新项目推荐使用 Lettuce,特别是 Spring Boot 2.x 以上版本
  • 老项目维护或简单场景可用 Jedis
  • 需要复杂分布式数据结构时考虑 Redisson

七、怎么选集群方案?

场景 推荐方案
小型项目,Redis 压力不大 单机或一主一从
需要自动故障恢复 主从 + 哨兵
数据量大,写压力高 Redis Cluster 分片集群
单节点内存超过 10G 优先考虑拆分服务或分片

面试可以这样说:

Redis 集群方案主要有主从复制、哨兵和分片集群。主从复制通过读写分离提升读并发;哨兵在主从基础上增加监控、自动故障转移和通知能力;分片集群通过多个 Master 分摊数据和写压力,并通过 16384 个哈希槽决定 key 的存储位置。脑裂可以通过限制最少从节点数量和最大复制延迟来降低风险。

相关推荐
海南java第二人3 小时前
ClickHouse 稀疏索引深度解析:为什么 OLAP 数据库不用 B-Tree?
数据库·clickhouse
Litluecat3 小时前
信创迁移:Oracle切换海量数据库,慢sql扫描
数据库·sql·oracle·信创·海量
消失在人海中3 小时前
Oracle的CURRENT REDO丢失,数据丢失风险分析
数据库·oracle
喵了几个咪4 小时前
选择第三方IAM还是自建权限体系?中小型后台系统权限架构决策指南
数据库·oracle·架构
Elastic 中国社区官方博客5 小时前
Kibana:使用 AI Chat 及 MCP 轻松创建 AI 原生仪表板
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·信息可视化
杨云龙UP5 小时前
Oracle Health Check巡检脚本使用SOP V2.0:从HTML原始报告→生成Word专业巡检报告→交付客户_2026-06-03
linux·运维·数据库·sql·oracle·报告·巡检
Database_Cool_5 小时前
Hudi 湖仓一体架构:阿里云 AnalyticDB MySQL 原生集成最佳实践
数据库·mysql·阿里云
我是一颗柠檬6 小时前
【Redis】发布订阅与消息队列Day8(2026年)
数据库·redis·后端·缓存