美团Java面试被问:Redis集群模式的工作原理

一、Redis 集群模式全景

1. 集群模式对比

text

复制代码
Redis 高可用方案演进:
├── 主从复制(Replication)
├── 哨兵模式(Sentinel)
└── 集群模式(Cluster)⭐ 主流方案

集群模式特点:
- 数据分片(Sharding):16384个哈希槽
- 高可用:主从自动故障转移
- 去中心化:Gossip协议通信
- 线性扩展:支持水平扩容

二、核心架构原理

1. 数据分片:哈希槽(Hash Slot)机制

bash

复制代码
# 哈希槽范围:0-16383(共16384个槽)
# 键值对分配到槽的算法:
CRC16(key) % 16384

# 查看键所在槽
redis-cli -c CLUSTER KEYSLOT "user:1001"
# 输出:5478(表示该键在5478号槽)

2. 集群节点角色

text

复制代码
集群节点类型:
├── 主节点(Master)
│   ├── 负责处理槽(0-16384的子集)
│   ├── 响应读写请求
│   └── 参与故障检测与选举
│
└── 从节点(Slave)
    ├── 复制主节点数据
    ├── 在主节点故障时升级为主节点
    └── 提供读请求(可配置)

每个主节点应有1-N个从节点
最小集群配置:3主3从

3. 集群元数据管理

java

复制代码
// 每个节点维护的集群状态
public class ClusterNode {
    /*
    节点信息(cluster nodes 命令输出):
    
    <node-id> <ip:port@cport> <flags> <master-id> 
    <ping-sent> <pong-recv> <config-epoch> 
    <link-state> <slot1> <slot2> ... <slotN>
    
    关键字段:
    - node-id: 160bit唯一标识(与IP:PORT绑定)
    - flags: master,slave,myself,fail?,handshake...
    - slots: 该节点负责的槽位范围
    - config-epoch: 配置纪元(故障转移用)
    */
}

三、集群搭建与数据分布

1. 集群创建过程

bash

复制代码
# 1. 准备6个节点(3主3从示例)
redis-server --port 7000 --cluster-enabled yes --cluster-config-file nodes-7000.conf
redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes-7001.conf
# ... 直到7005

# 2. 创建集群(Redis 5.0+)
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

# 3. 集群会自动分配槽位
# 主节点:7000(0-5460), 7001(5461-10922), 7002(10923-16383)
# 从节点:7003复制7000,7004复制7001,7005复制7002

2. 槽位分配算法

java

复制代码
// 槽位分配的核心逻辑
public class SlotAllocation {
    /*
    初始分配流程:
    
    1. 为每个主节点生成一个长度为16384的位图
    2. 按顺序轮流分配槽位:
       节点A: 0, 3, 6, 9...
       节点B: 1, 4, 7, 10...
       节点C: 2, 5, 8, 11...
    
    3. 最终每个主节点大约分配 16384/N 个槽
    
    扩容时重新分配:
    1. 计算每个节点应该持有的槽数 = 16384/(新节点数)
    2. 从现有节点迁移部分槽到新节点
    3. 迁移过程中保证数据可用性
    */
}

3. 查看集群状态

bash

复制代码
# 查看集群信息
redis-cli -p 7000 cluster info
# 输出:
# cluster_state:ok           # 集群状态
# cluster_slots_assigned:16384  # 已分配槽数
# cluster_slots_ok:16384     # 正常槽数
# cluster_known_nodes:6      # 已知节点数
# cluster_size:3             # 主节点数

# 查看节点信息
redis-cli -p 7000 cluster nodes

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】


四、客户端请求处理流程

1. 智能客户端 vs 代理模式

java

复制代码
// 智能客户端实现原理
public class SmartRedisClient {
    
    // 客户端维护的槽位映射表
    private Map<Integer, RedisNode> slotCache = new HashMap<>();
    
    public Object executeCommand(String key, Command command) {
        // 1. 计算key所在的槽
        int slot = CRC16.crc16(key.getBytes()) % 16384;
        
        // 2. 从缓存获取槽对应的节点
        RedisNode targetNode = slotCache.get(slot);
        if (targetNode == null) {
            // 3. 缓存未命中,通过集群获取映射
            targetNode = refreshSlotMapping();
            slotCache.put(slot, targetNode);
        }
        
        // 4. 发送命令到目标节点
        return sendToNode(targetNode, command);
    }
    
    // 处理MOVED/ASK重定向
    private Object handleRedirect(RedirectResponse response) {
        if (response.getType() == RedirectType.MOVED) {
            // MOVED:永久重定向,更新槽位映射
            slotCache.put(response.getSlot(), response.getNewNode());
            // 重新执行命令
            return retryCommand(response);
        } else if (response.getType() == RedirectType.ASK) {
            // ASK:临时重定向,不更新映射
            // 先发送ASKING命令,再重试
            sendASKING(response.getNewNode());
            return retryCommand(response);
        }
    }
}

2. MOVED vs ASK 重定向

java

复制代码
// 两种重定向的区别
public class RedirectMechanism {
    /*
    MOVED 重定向(永久):
    场景:槽已经迁移到其他节点
    客户端响应:更新本地槽位映射缓存
    示例:MOVED 3999 127.0.0.1:6381
    
    ASK 重定向(临时):
    场景:槽正在迁移中(一部分数据在新节点)
    客户端响应:不更新缓存,只本次请求重定向
    示例:ASK 3999 127.0.0.1:6381
    
    关键区别:
    1. 持久性:MOVED更新缓存,ASK不更新
    2. 时机:槽完全迁移完 vs 迁移过程中
    3. 后续请求:MOVED后直接走新节点,ASK后可能还要问老节点
    */
}

3. 跨槽操作限制

bash

复制代码
# Redis集群不支持跨槽操作
# 以下操作会报错:CROSSSLOT Keys in request don't hash to same slot

# 错误示例:MSET key1 val1 key2 val2
# 如果key1和key2在不同槽,会失败

# 解决方案1:使用hash tag
MSET {user:1001}:name "张三" {user:1001}:age 25
# 大括号{}中的内容用于计算槽位,确保相同user在同一个槽

# 解决方案2:客户端分批处理
# 解决方案3:使用Lua脚本(但脚本内的key也必须在同一节点)

五、故障检测与转移

1. Gossip 协议通信

java

复制代码
// 节点间通过Gossip协议交换信息
public class GossipProtocol {
    /*
    通信机制:
    
    1. PING/PONG消息
       - 每个节点每秒随机选择几个节点发送PING
       - 收到PING的节点回复PONG
       - 消息包含:自身状态 + 已知的其他节点状态
    
    2. 故障检测(主观下线 + 客观下线)
       - 主观下线:节点A在cluster-node-timeout内未收到节点B的PONG
       - 客观下线:超过半数主节点认为某节点主观下线
    
    3. 信息传播
       - 通过PING/PONG传播集群状态变更
       - 最终一致性:信息最终会传播到所有节点
    */
}

2. 故障转移流程

java

复制代码
// 自动故障转移实现
public class FailoverProcess {
    /*
    故障转移触发条件:
    1. 主节点被标记为客观下线(FAIL状态)
    2. 该主节点有从节点
    3. 从节点与主节点复制连接正常
    
    选举流程:
    
    1. 资格检查
       - 从节点与主节点断开时间 < cluster-node-timeout * cluster-slave-validity-factor
       - 从节点的复制偏移量较新
    
    2. 发起选举
       - 从节点增加配置纪元(configEpoch)
       - 向其他主节点请求投票
    
    3. 投票机制
       - 每个主节点在每个配置纪元只能投一票
       - 收到超过半数主节点投票的从节点胜出
    
    4. 故障转移
       - 胜出的从节点执行SLAVEOF NO ONE
       - 接管原主节点的槽位
       - 广播新的配置信息
    */
}

3. 配置纪元(Config Epoch)

bash

复制代码
# 配置纪元:集群的"逻辑时钟"
# 用于解决脑裂和配置冲突

# 查看节点的配置纪元
redis-cli -p 7000 cluster nodes | grep myself
# 输出包含:... 127.0.0.1:7000 myself,master - 0 0 1 connected 0-5460
# 最后的"1"就是配置纪元

# 配置纪元的作用:
# 1. 故障转移时递增,确保新主节点有更高的epoch
# 2. 解决网络分区恢复后的主从冲突
# 3. 保证配置信息的有序传播

六、数据迁移与扩容

1. 槽迁移流程

bash

复制代码
# 集群扩容:添加新节点
# 1. 添加空节点
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000

# 2. 分配槽位(从现有节点迁移)
redis-cli --cluster reshard 127.0.0.1:7000
# 交互式提示:
# How many slots do you want to move? 4096  # 要迁移的槽数
# What is the receiving node ID? [新节点ID]
# Source node #1: all  # 从所有现有节点平均迁移
# 或指定具体节点:[原节点ID]

# 3. 添加从节点
redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7000 --cluster-slave --cluster-master-id [新主节点ID]

2. 迁移过程数据一致性

java

复制代码
// 迁移过程中的读写处理
public class SlotMigration {
    /*
    迁移步骤:
    
    1. 设置迁移状态
       CLUSTER SETSLOT <slot> IMPORTING <source-node-id>
       CLUSTER SETSLOT <slot> MIGRATING <target-node-id>
    
    2. 迁移数据
       - 对源节点:MIGRATE命令批量迁移key
       - 迁移过程中,源节点继续处理请求
    
    3. 请求处理规则:
       源节点收到请求:
       - 如果key存在且未迁移:正常处理
       - 如果key不存在(已迁移):返回ASK重定向
       - 如果key存在但正在迁移:阻塞客户端,迁移完成后返回
    
       目标节点收到请求:
       - 只有先收到ASKING命令后,才会处理重定向请求
    
    4. 完成迁移
       - 迁移完成后,广播槽位所有权变更
       - 所有节点更新槽位映射
    */
}

3. 重新分片脚本示例

bash

复制代码
#!/bin/bash
# 自动化重新分片脚本

CLUSTER_HOST="127.0.0.1:7000"
NEW_NODE="127.0.0.1:7006"
SLOTS_PER_NODE=4096

# 获取集群节点信息
NODES=$(redis-cli -c -h ${CLUSTER_HOST%:*} -p ${CLUSTER_HOST#*:} cluster nodes | grep master | awk '{print $2}')

# 为每个现有节点迁移部分槽到新节点
for NODE in $NODES; do
    HOST=${NODE%:*}
    PORT=${NODE#*:}
    
    echo "从节点 $HOST:$PORT 迁移 $SLOTS_PER_NODE 个槽到新节点"
    
    # 执行迁移
    redis-cli --cluster reshard $HOST:$PORT \
        --cluster-from $(redis-cli -h $HOST -p $PORT cluster myid) \
        --cluster-to $(redis-cli -h ${NEW_NODE%:*} -p ${NEW_NODE#*:} cluster myid) \
        --cluster-slots $SLOTS_PER_NODE \
        --cluster-yes
done

七、集群运维与监控

1. 关键监控指标

bash

复制代码
# 集群健康检查
redis-cli --cluster check 127.0.0.1:7000

# 监控指标采集
redis-cli -p 7000 info cluster
# 关键指标:
# cluster_state:ok                    # 集群状态
# cluster_slots_assigned:16384       # 已分配槽数
# cluster_slots_ok:16384             # 正常槽数
# cluster_slots_pfail:0              # 可能失败槽数
# cluster_slots_fail:0               # 已失败槽数
# cluster_known_nodes:6              # 节点数
# cluster_size:3                     # 主节点数

# 节点延迟监控
redis-cli -p 7000 cluster nodes | awk '{print $1,$6,$7}'
# 输出:节点ID ping发送时间 pong接收时间

2. 运维命令大全

bash

复制代码
# 1. 集群管理
redis-cli --cluster help  # 查看所有集群命令

# 2. 节点管理
redis-cli --cluster add-node <new_host:port> <existing_host:port>  # 添加节点
redis-cli --cluster del-node <host:port> <node_id>                 # 删除节点
redis-cli --cluster call <host:port> command arg1 arg2 ...         # 在所有节点执行命令

# 3. 槽位管理
redis-cli --cluster reshard <host:port>                            # 重新分片
redis-cli --cluster rebalance <host:port> --cluster-threshold 2    # 平衡槽位
redis-cli --cluster fix <host:port>                                # 修复槽位

# 4. 数据迁移
redis-cli --cluster import <host:port> --cluster-from <source_host:port> --cluster-copy

# 5. 故障处理
redis-cli --cluster failover <host:port> --cluster-master-id <node_id>  # 手动故障转移

3. 常见故障处理

bash

复制代码
# 场景1:节点宕机恢复
# 如果主节点宕机后恢复,会变成从节点
# 手动恢复为主节点:
redis-cli -p 7000 cluster failover --force

# 场景2:脑裂问题
# 网络分区导致集群分裂,手动修复:
redis-cli --cluster fix 127.0.0.1:7000

# 场景3:槽位丢失
# 某些槽没有节点负责:
redis-cli --cluster fix 127.0.0.1:7000 --cluster-search-multiple-owners

# 场景4:从节点无法同步
# 检查主从连接状态:
redis-cli -p 7003 info replication
# 如果断开,手动重新同步:
redis-cli -p 7003 cluster replicate <master-node-id>

4. 性能优化配置

bash

复制代码
# redis.conf 集群优化配置

# 网络相关
cluster-node-timeout 15000          # 节点超时时间(毫秒)
cluster-slave-validity-factor 10    # 从节点有效因子
cluster-require-full-coverage yes   # 是否需要所有槽都覆盖

# 迁移优化
cluster-migration-barrier 1         # 主节点最少从节点数
cluster-replica-validity-factor 10  # 从节点复制有效性

# 性能优化
repl-disable-tcp-nodelay no         # 主从复制启用Nagle算法
client-output-buffer-limit slave 256mb 64mb 60  # 从节点缓冲区
client-output-buffer-limit pubsub 32mb 8mb 60   # 发布订阅缓冲区

八、集群限制与解决方案

1. Redis 集群的限制

java

复制代码
public class ClusterLimitations {
    /*
    1. 不支持跨节点事务
       - MULTI/EXEC中的key必须在同一节点
       - 解决方案:使用hash tag确保key在同一槽
    
    2. 不支持跨节点Lua脚本
       - 脚本中所有key必须在同一节点
       - 解决方案:使用EVALSHA + hash tag
    
    3. 键批量操作限制
       - MGET/MSET等需要key在同一节点
       - 解决方案:客户端分批或使用hash tag
    
    4. 数据库数量限制
       - 集群只支持db0
       - 解决方案:用key前缀区分业务
    
    5. Pub/Sub限制
       - 发布订阅消息不会跨节点广播
       - 解决方案:客户端连接到所有节点,或使用Redis Stream
    */
}

2. 生产环境最佳实践

yaml

复制代码
# 集群部署规范
集群规模:
  最小配置: 3主3从
  推荐配置: 6主6从(生产环境)
  最大规模: 1000节点(理论限制)

硬件配置:
  内存: 主节点≤30GB(避免RDB/AOF过慢)
  网络: 万兆网卡(迁移数据需要)
  CPU: 至少4核(加密、压缩需要)

监控告警:
  必须监控:
    - 集群状态: cluster_state
    - 槽位覆盖: cluster_slots_ok
    - 节点数: cluster_known_nodes
    - 内存使用率
    - 网络延迟
  
  告警阈值:
    - 集群状态 != ok
    - 槽位覆盖 < 16384
    - 节点失联 > 1分钟
    - 内存使用 > 80%

3. 与代理模式(Codis/Twemproxy)对比

markdown

复制代码
| 特性 | Redis Cluster | Codis/Twemproxy |
|------|--------------|-----------------|
| 架构 | 去中心化,智能客户端 | 中心化代理 |
| 数据分片 | 客户端计算 | 代理计算 |
| 扩容 | 在线迁移,自动平衡 | 需要手动迁移,重启 |
| 运维复杂度 | 较高(需要理解集群协议) | 较低(简单代理) |
| 客户端支持 | 需要集群感知客户端 | 任何Redis客户端 |
| 故障转移 | 自动(秒级) | 依赖ZooKeeper/Etcd |
| 跨槽操作 | 不支持 | 支持(代理层处理) |

选择建议:
- 新项目:推荐Redis Cluster(官方维护,功能完整)
- 旧系统迁移:Codis可能更平滑
- 需要跨槽操作:考虑代理模式

九、客户端实现示例

1. Java客户端(Lettuce)

java

复制代码
// Lettuce 集群客户端配置
@Configuration
public class RedisClusterConfig {
    
    @Value("${spring.redis.cluster.nodes}")
    private String clusterNodes;
    
    @Value("${spring.redis.timeout}")
    private Duration timeout;
    
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisClusterConfiguration config = new RedisClusterConfiguration();
        
        // 解析节点配置:host1:port1,host2:port2
        String[] nodes = clusterNodes.split(",");
        for (String node : nodes) {
            String[] hostPort = node.split(":");
            config.addClusterNode(new RedisNode(hostPort[0], 
                Integer.parseInt(hostPort[1])));
        }
        
        // 连接池配置
        GenericObjectPoolConfig<Object> poolConfig = 
            new GenericObjectPoolConfig<>();
        poolConfig.setMaxTotal(8);
        poolConfig.setMaxIdle(8);
        poolConfig.setMinIdle(0);
        
        // 客户端配置
        LettuceClientConfiguration clientConfig = 
            LettucePoolingClientConfiguration.builder()
                .commandTimeout(timeout)
                .poolConfig(poolConfig)
                .build();
        
        return new LettuceConnectionFactory(config, clientConfig);
    }
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

2. 连接池优化配置

yaml

复制代码
# application.yml 集群配置
spring:
  redis:
    cluster:
      nodes: 192.168.1.101:7000,192.168.1.102:7000,192.168.1.103:7000
      max-redirects: 3  # 最大重定向次数
    lettuce:
      pool:
        max-active: 8    # 连接池最大连接数
        max-idle: 8      # 最大空闲连接
        min-idle: 0      # 最小空闲连接
        max-wait: -1ms   # 获取连接最大等待时间
      cluster:
        refresh:
          adaptive: true  # 自适应刷新拓扑
          period: 2000ms  # 刷新周期
    timeout: 2000ms       # 连接超时

3. 客户端最佳实践

java

复制代码
// 生产环境客户端使用建议
public class RedisClusterBestPractice {
    
    // 1. 使用连接池,避免频繁创建连接
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    
    // 2. 批量操作使用pipeline(同一节点)
    public void batchOperations() {
        List<String> keys = Arrays.asList("{user:1001}:name", 
                                          "{user:1001}:age");
        // 使用hash tag确保在同一节点
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            for (String key : keys) {
                connection.get(key.getBytes());
            }
            return null;
        });
    }
    
    // 3. 处理重定向异常
    public Object safeGet(String key) {
        try {
            return redisTemplate.opsForValue().get(key);
        } catch (RedisClusterException e) {
            if (e.getMessage().contains("MOVED") || 
                e.getMessage().contains("ASK")) {
                // 客户端会自动处理重定向
                // 记录日志用于监控
                log.warn("Redis集群重定向: {}", e.getMessage());
                // 重试一次
                return redisTemplate.opsForValue().get(key);
            }
            throw e;
        }
    }
    
    // 4. 监控连接状态
    @Scheduled(fixedDelay = 30000)
    public void checkClusterHealth() {
        try {
            String clusterInfo = redisTemplate
                .getConnectionFactory()
                .getConnection()
                .clusterInfo();
            log.info("集群状态: {}", clusterInfo);
        } catch (Exception e) {
            log.error("集群健康检查失败", e);
            // 触发告警
        }
    }
}

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】


十、总结

Redis Cluster 核心要点:

markdown

复制代码
1. **数据分片**:16384个哈希槽,CRC16(key) % 16384
2. **高可用**:主从复制 + 自动故障转移
3. **去中心化**:Gossip协议通信,无单点故障
4. **线性扩展**:支持在线扩容缩容

适用场景:
✅ 大数据量(单机内存不足)
✅ 高并发读写
✅ 需要高可用
✅ 预算有限(相比Codis无代理层开销)

不适用场景:
❌ 需要跨节点事务
❌ 大量使用Lua脚本(跨节点)
❌ 对运维复杂度敏感的小团队

生产环境部署清单:

bash

复制代码
# 部署前检查清单
□ 1. 节点数:至少3主3从
□ 2. 内存:每个节点≤30GB
□ 3. 网络:节点间延迟<5ms
□ 4. 配置:cluster-node-timeout合理设置
□ 5. 监控:集群状态、槽位覆盖、节点健康
□ 6. 备份:定期RDB/AOF备份
□ 7. 客户端:使用支持集群的智能客户端
□ 8. 压测:模拟故障转移,验证高可用

最终建议:Redis Cluster 是目前最成熟、最推荐的Redis分布式方案。理解其核心原理(槽分片、Gossip协议、故障转移)对于生产环境运维至关重要。对于新项目,建议直接使用Redis Cluster;对于已有系统迁移,需要评估业务是否使用了集群不支持的特性。

相关推荐
一雨方知深秋2 小时前
面向对象编程
java·封装·this·构造器·static关键字·成员变量·javabean实体类
资生算法程序员_畅想家_剑魔2 小时前
Java常见技术分享-11-责任链模式
java·spring boot·责任链模式
计算机程序设计小李同学2 小时前
动漫之家系统设计与实现
java·spring boot·后端·web安全
布列瑟农的星空3 小时前
SSE与流式传输(Streamable HTTP)
前端·后端
程序员阿鹏3 小时前
责任链模式
java·spring·servlet·tomcat·maven·责任链模式
开心就好20253 小时前
使用 HBuilder 上架 iOS 应用时常见的问题与应对方式
后端
@淡 定3 小时前
Java内存模型(JMM)详解
java·开发语言
开心猴爷3 小时前
Swift IPA 混淆在工程实践中的方式,分析仅依赖源码层混淆的局限性
后端
支撑前端荣耀3 小时前
从零实现前端监控告警系统:SMTP + Node.js + 个人邮箱 完整免费方案
前端·javascript·面试