问题
Redis的集群模式是什么?
答案
1. 核心概念
Redis Cluster(集群模式)是Redis官方提供的分布式解决方案,通过数据分片(Sharding)实现水平扩展,解决单机内存和性能瓶颈。
核心特性:
- 去中心化:无需代理节点(如Codis的Proxy),客户端直接访问数据节点
- 数据分片:将数据分散到多个主节点,每个主节点负责一部分数据
- 高可用:支持主从复制和自动故障转移
- 横向扩展:可在线增减节点,动态调整槽位分配
2. 集群架构
客户端
|
| (1) 计算key所属slot
v
+-----+-----+-----+
| | | |
Master1 M2 M3 (主节点,各负责部分slot)
| | |
Slave1 S2 S3 (从节点,主从复制)
Master1: slot 0-5460
Master2: slot 5461-10922
Master3: slot 10923-16383
关键要点:
- 最少6个节点:3主3从,保证高可用
- 16384个槽位(slot):每个key通过CRC16哈希后对16384取模,映射到对应槽位
- 槽位分配:每个主节点负责一段连续或分散的槽位
3. 数据分片机制
3.1 槽位(Slot)算法
Redis Cluster将数据空间划分为 16384个槽位(0-16383),每个key根据以下算法映射到槽位:
// 槽位计算算法
public class RedisClusterSlot {
private static final int SLOT_COUNT = 16384;
public static int calculateSlot(String key) {
// 1. 检查是否有HashTag(用于将多个key映射到同一slot)
String hashKey = getHashTagKey(key);
// 2. CRC16计算并对16384取模
int slot = CRC16.crc16(hashKey.getBytes()) % SLOT_COUNT;
return slot;
}
// HashTag提取:{user:1001}:info -> user:1001
private static String getHashTagKey(String key) {
int start = key.indexOf('{');
if (start != -1) {
int end = key.indexOf('}', start + 1);
if (end != -1 && end > start + 1) {
return key.substring(start + 1, end);
}
}
return key;
}
}
HashTag用途:
// 场景:保证同一用户的多个key在同一节点,支持批量操作
jedis.mset(
"{user:1001}:name", "Alice",
"{user:1001}:age", "30"
);
// 两个key的HashTag都是"user:1001",映射到同一slot
3.2 槽位分配策略
初始化集群时:
# 创建集群(Redis 5.0+)
redis-cli --cluster create \
192.168.1.10:7000 192.168.1.11:7000 192.168.1.12:7000 \
192.168.1.10:7001 192.168.1.11:7001 192.168.1.12:7001 \
--cluster-replicas 1 # 每个主节点1个从节点
# 输出示例:
# Master1 (7000): slots 0-5460
# Master2 (7000): slots 5461-10922
# Master3 (7000): slots 10923-16383
在线迁移槽位:
# 迁移slot 100从节点A到节点B
redis-cli --cluster reshard 192.168.1.10:7000 \
--cluster-from <nodeA_id> \
--cluster-to <nodeB_id> \
--cluster-slots 1 \
--cluster-yes
4. 节点通信机制(Gossip协议)
4.1 Gossip协议
Redis Cluster节点通过 Gossip协议 交换集群状态信息:
节点A 节点B 节点C
| | |
|-- PING ----------->| |
|<-- PONG -----------| |
| |-- PING ----------->|
| |<-- PONG -----------|
|-- MEET ----------->| |
| |-- MEET ----------->|
消息类型:
- PING/PONG:心跳检测,携带自身状态和部分其他节点信息
- MEET:通知节点加入集群
- FAIL:广播节点下线消息
通信端口:
- 数据端口:如7000,用于客户端读写
- 集群总线端口:数据端口+10000(如17000),用于节点间通信
4.2 槽位信息同步
每个节点维护 槽位映射表:
Slot 0-5460 -> 节点A (192.168.1.10:7000)
Slot 5461-10922 -> 节点B (192.168.1.11:7000)
Slot 10923-16383 -> 节点C (192.168.1.12:7000)
客户端请求流程:
- 客户端连接任意节点,计算key所属slot
- 若slot不在当前节点,返回
MOVED <slot> <ip>:<port>重定向 - 客户端缓存槽位映射表,后续请求直接访问正确节点
// Jedis客户端示例
JedisCluster jedis = new JedisCluster(
new HostAndPort("192.168.1.10", 7000)
);
// 自动处理MOVED重定向
jedis.set("key1", "value1");
5. 故障检测与转移
5.1 故障检测
主观下线(PFAIL):
- 单个节点认为目标节点下线
- 条件:超过
cluster-node-timeout未收到PONG回复(默认15秒)
客观下线(FAIL):
- 半数以上主节点认为目标节点下线
- 触发自动故障转移
5.2 自动故障转移
// 故障转移流程(伪代码)
public class ClusterFailover {
public void execute() {
// 1. 主节点A下线,其从节点发起选举
Slave slave = findQualifiedSlave();
// 2. 从节点向其他主节点请求投票
int votes = requestVotes();
// 3. 获得半数以上投票,升级为主节点
if (votes > totalMasters / 2) {
slave.promoteToMaster();
}
// 4. 接管原主节点的槽位
takeOverSlots();
// 5. 广播PONG消息,通知集群配置变更
broadcastConfigUpdate();
}
}
选主条件(优先级从高到低):
- 数据完整性:复制偏移量最大的从节点(数据最新)
- runID最小:保证确定性选举
6. 集群扩缩容
6.1 扩容(添加节点)
# 1. 启动新节点(主+从)
redis-server /path/to/7003.conf
redis-server /path/to/7004.conf
# 2. 添加主节点到集群
redis-cli --cluster add-node 192.168.1.13:7003 192.168.1.10:7000
# 3. 迁移部分槽位到新节点
redis-cli --cluster reshard 192.168.1.10:7000 \
--cluster-from <source_node_id> \
--cluster-to <new_node_id> \
--cluster-slots 2000 # 迁移2000个slot
# 4. 添加从节点
redis-cli --cluster add-node 192.168.1.13:7004 192.168.1.10:7000 \
--cluster-slave --cluster-master-id <master_node_id>
6.2 缩容(删除节点)
# 1. 迁移待删除节点的槽位到其他节点
redis-cli --cluster reshard 192.168.1.10:7000 \
--cluster-from <node_to_remove_id> \
--cluster-to <target_node_id> \
--cluster-slots <all_slots>
# 2. 删除从节点
redis-cli --cluster del-node 192.168.1.13:7004 <node_id>
# 3. 删除主节点(确保槽位已全部迁移)
redis-cli --cluster del-node 192.168.1.13:7003 <node_id>
7. 集群限制与注意事项
7.1 多键操作限制
问题:集群模式下,多键操作(如MGET、MSET)要求所有key在同一slot
解决方案:
// 使用HashTag保证key在同一slot
jedis.mget("{user:1001}:name", "{user:1001}:age");
// 或使用Pipeline批量操作(允许跨slot)
Pipeline pipeline = jedis.pipelined();
pipeline.get("key1");
pipeline.get("key2");
pipeline.sync();
7.2 事务与Lua脚本限制
- 事务(MULTI/EXEC):所有key必须在同一slot
- Lua脚本:所有操作的key必须在同一slot
- 解决方案:使用HashTag或设计时避免跨slot操作
7.3 性能考量
优势:
- 线性扩展:增加节点提升整体吞吐量
- 无代理层:减少网络开销
劣势:
- 网络通信开销:Gossip协议定期交换信息
- 数据倾斜:热点slot导致某些节点负载高(可通过调整slot分配缓解)
8. 面试答题总结
标准回答模板:
Redis Cluster是官方分布式方案,通过 槽位(Slot)分片 实现水平扩展:
- 数据分片:16384个槽位,key通过
CRC16(key) % 16384映射到槽位,每个主节点负责一部分槽位- 去中心化:节点通过Gossip协议交换状态,客户端直连数据节点(无需Proxy)
- 故障转移:主节点下线时,从节点自动发起选举并接管槽位
- 在线扩容:通过
redis-cli --cluster reshard迁移槽位,无需停机核心优势:
- 官方支持,无需第三方组件
- 自动分片和故障转移
- 可线性扩展到1000+节点
注意事项:
- 多键操作需使用HashTag(如
{user:1001}:field)- 最少6个节点(3主3从)
常见追问:
- 为什么是16384个槽位而不是更多? → CRC16输出16位,16384是2^14,压缩心跳包大小(槽位位图2KB),且足够满足大规模集群
- 集群模式和哨兵模式的区别? → 哨兵解决高可用(单主),集群解决扩展性(多主分片)
- 如何解决数据倾斜? → 调整槽位分配、使用HashTag分散热点key、增加节点