Redis Cluster 集群原理:数据分片与故障转移
Redis Cluster 集群原理:数据分片与故障转移
前言
Redis Cluster 是 Redis 官方提供的分布式解决方案,在 Redis 3.0 版本中正式引入。它通过数据分片(Sharding)实现了水平扩展,通过自动故障转移(Failover)保证了高可用性。本文将深入剖析 Redis Cluster 的核心原理,包括数据分片机制、故障转移流程、以及源码实现细节。
标签: Redis, Cluster, 集群, 分片, 故障转移
一、Redis Cluster 架构概览
1.1 集群拓扑结构
Redis Cluster 采用去中心化的架构,没有专门的配置服务器或元数据服务器。所有节点通过 Gossip 协议进行通信,每个节点都保存了整个集群的状态信息。
核心特点:
- 16384 个哈希槽:整个集群的数据库被分为 16384 个槽(slot)
- 节点分配槽位:每个 Master 节点负责处理一定范围的槽
- 自动故障转移:当 Master 节点故障时,Replica 会自动晋升为 Master
- 去中心化:所有节点地位平等,通过 Gossip 协议交换状态
1.2 与其他集群方案对比
| 特性 | Redis Cluster | Redis Sentinel | Twemproxy | Codis |
|---|---|---|---|---|
| 架构类型 | 去中心化 | 中心化代理 | 中心化代理 | 中心化代理 |
| 数据分片 | Hash Slot | 无 | 一致性哈希 | Hash Slot |
| 高可用 | 自动故障转移 | 自动故障转移 | 需配合 Sentinel | 支持 |
| 水平扩展 | 动态扩缩容 | 不支持 | 静态配置 | 动态扩缩容 |
| 客户端 | Smart Client | 普通客户端 | 普通客户端 | 普通客户端 |
| 性能损耗 | 几乎无 | 单节点 | 代理转发 | 代理转发 |
| 运维复杂度 | 中等 | 较低 | 较高 | 较高 |
二、数据分片机制:Hash Slot
2.1 哈希槽原理
Redis Cluster 将所有数据映射到 16384 个哈希槽(slot)中:
// cluster.c (Redis 7.2.0)
#define CLUSTER_SLOTS 16384
// 计算键所在的槽位
unsigned int keyHashSlot(char *key, int keylen) {
int s, e; /* start-end indexes of { and } */
/* 查找 {} 包围的哈希标签 */
for (s = 0; s < keylen; s++)
if (key[s] == '{') break;
/* 如果有哈希标签,只计算标签内的哈希值 */
if (s != keylen && keylen > s+1) {
for (e = s+1; e < keylen; e++)
if (key[e] == '}') break;
/* 只有当 } 存在时才使用标签 */
if (e != keylen && e > s+1) {
keylen = e - s - 1;
key = key + s + 1;
}
}
/* CRC16 算法计算槽位 */
return crc16(key, keylen) & (CLUSTER_SLOTS - 1);
}
为什么是 16384?
- 心跳包大小:每个节点需要通过 Gossip 协议交换槽位分配信息,16384 个槽位占用 2KB(16384/8 = 2048 字节),心跳包大小适中
- 位图效率:2048 字节可以方便地用位图(bitmap)表示,便于快速判断槽位分配
- 内存开销:如果槽数太多(如 65536),位图将占用 8KB,增加网络传输开销
2.2 数据分布流程
2.3 哈希标签(Hash Tags)
功能: 确保相关联的键分配到同一个槽位,支持 MGET、MSET、事务等操作。
规则: 只计算 {} 内的部分
# 这些键会被分配到同一个槽位
SET {user:1001}:name "Alice"
SET {user:1001}:age 25
SET {user:1001}:city "Beijing"
# 事务操作可以正常执行
MULTI
GET {user:1001}:name
GET {user:1001}:age
EXEC
源码分析:
// cluster.c (Redis 7.2.0) - 判断是否支持多键操作
int clusterNodeSupportsMultiKey(int node_id, robj **keys, int count) {
int slot = -1;
for (int i = 0; i < count; i++) {
int keyslot = keyHashSlot(
keys[i]->ptr,
sdslen(keys[i]->ptr)
);
if (slot == -1) {
slot = keyslot;
} else if (slot != keyslot) {
/* 键不在同一个槽位 */
return 0;
}
}
return 1;
}
三、故障转移机制
3.1 节点故障检测
Redis Cluster 通过 Gossip 协议和 PFAIL/FAIL 状态机实现故障检测。
关键时间参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
cluster-node-timeout |
15000ms | 超过此时间未收到心跳,标记为 PFAIL |
cluster-slave-validity-factor |
10 | 从节点断连时间超过 timeout * factor,不参与故障转移 |
cluster-migration-barrier |
1 | 主节点最少拥有的从节点数 |
3.2 故障转移流程
当 Master 被标记为 FAIL 后,其从节点会进行选举:
// cluster.c (Redis 7.2.0) - 从节点发起选举
void clusterStartManualFailoverIfNeeded(clusterNode *target) {
/* 1. 检查是否具备选举资格 */
if (nodeIsSlave(myself) &&
myself->slaveof == target &&
server.cluster_allow_replica_migration) {
/* 2. 计算选举延迟 */
mstime_t delay = mstime() +
500 + /* 固定延迟 */
random() % 500; /* 随机延迟 */
/* 3. 发起选举 */
clusterSendFailoverAuthIfNeeded();
}
}
// 请求其他 Master 节点投票
void clusterSendFailoverAuthIfNeeded(clusterNode *node) {
clusterMsg *msg;
clusterMsgDataFailoverAuth *auth;
/* 构造 FAILOVER_AUTH_REQUEST 请求 */
msg = createClusterMessage(CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST);
auth = &msg->data.failover_auth;
auth->configEpoch = node->configEpoch;
auth->offset = node->replication_offset;
/* 广播到所有节点 */
clusterBroadcastMessage(msg);
}
完整故障转移流程:
3.3 选举算法详解
Redis Cluster 使用 Raft-like 选举算法:
投票规则(从节点视角):
- 只能投票一次:每个
configEpoch只能给一个从节点投票 - 优先级规则:
- 复制偏移量更大的优先
replica-priority配置更小的优先(默认 100)- 节点 ID 更小的优先
// cluster.c (Redis 7.2.0) - Master 处理投票请求
int clusterProcessFailoverAuthRequest(clusterNode *sender,
clusterMsg *req) {
clusterNode *master = sender->slaveof;
/* 1. 检查是否已经投过票 */
if (myself->failover_auth_sent > 0 &&
myself->failover_auth_epoch == req->currentEpoch) {
return 0; /* 本轮已投票 */
}
/* 2. 检查是否可以投票 */
if (master->flags & CLUSTER_NODE_FAIL) {
/* 3. 比较优先级 */
if (req->offset > master->replication_offset) {
/* 授权投票 */
myself->failover_auth_sent = 1;
myself->failover_auth_epoch = req->currentEpoch;
/* 发送 FAILOVER_AUTH_ACK */
clusterSendFailoverAuthAck(sender);
return 1;
}
}
return 0;
}
3.4 故障转移配置对比
| 配置项 | 推荐值 | 作用 |
|---|---|---|
cluster-node-timeout |
5000-15000ms | 故障检测超时时间 |
cluster-replica-validity-factor |
0 | 生产环境设为 0,避免从节点因短暂断连失去选举资格 |
cluster-migration-barrier |
1 | 允许自动迁移的阈值 |
cluster-require-full-coverage |
no | 节点故障时是否继续提供部分服务 |
典型场景配置:
# 生产环境配置
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-replica-validity-factor 0
cluster-migration-barrier 1
cluster-require-full-coverage no
四、集群操作实战
4.1 槽位分配算法
手动分配槽位:
# 将槽位 0-5460 分配给节点 127.0.0.1:7000
redis-cli -c -p 7000 cluster addslots {0..5460}
# 将槽位 5461-10922 分配给节点 127.0.0.1:7001
redis-cli -c -p 7001 cluster addslots {5461..10922}
# 将槽位 10923-16383 分配给节点 127.0.0.1:7002
redis-cli -c -p 7002 cluster addslots {10923..16383}
自动平衡槽位:
# 使用 redis-trib 工具(Redis 5.0+)
redis-cli --cluster rebalance 127.0.0.1:7000 \
--cluster-threshold 1 \ # 槽位差异阈值
--cluster-use-empty-masters
4.2 槽位迁移流程
源码实现:
// cluster.c (Redis 7.2.0) - 迁移单个键
void migrateCommand(client *c) {
char *keyword;
robj *key;
/* 解析参数: MIGRATE host port key dbid timeout */
keyword = c->argv[3]->ptr;
key = c->argv[4];
/* 1. 检查键是否在当前槽位 */
int slot = keyHashSlot(key->ptr, sdslen(key->ptr));
if (getShardNodeBySlot(slot) != myself) {
addReplySds(c, sdsnew("-NOAUTH Keys must belong to this node\r\n"));
return;
}
/* 2. 序列化键值对 */
rio payload;
createObjectPayload(&payload, key);
/* 3. 连接目标节点 */
int fd = connectToTarget(c->argv[1]->ptr, atoi(c->argv[2]->ptr));
/* 4. 发送 RESTORE 命令 */
char *cmd = sdscatprintf(sdsempty(), "*2\r\n$6\r\nMIGRATE\r\n$%zu\r\n%s\r\n",
sdslen(payload.io.buffer.ptr),
payload.io.buffer.ptr);
write(fd, cmd, sdslen(cmd));
/* 5. 等待确认 */
char reply[1024];
read(fd, reply, sizeof(reply));
/* 6. 删除本地键 */
dbDelete(&server.db[0], key);
addReply(c, shared.ok);
}
4.3 扩容缩容实战
新增节点:
# 1. 启动新节点
redis-server --port 7006 --cluster-enabled yes
# 2. 加入集群
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000
# 3. 分配槽位
redis-cli --cluster reshard 127.0.0.1:7000 \
--cluster-from <node_id> \
--cluster-to <new_node_id> \
--cluster-slots 4096 \
--cluster-yes
# 4. 配置复制关系
redis-cli -p 7006 cluster replicate <master_id>
缩容节点:
# 1. 迁移槽位
redis-cli --cluster reshard 127.0.0.1:7000 \
--cluster-from <node_id> \
--cluster-to <target_node_id> \
--cluster-slots 16384 \
--cluster-yes
# 2. 忘记节点
redis-cli --cluster del-node 127.0.0.1:7000 <node_id>
五、客户端实现原理
5.1 Smart Client 原理
Redis Cluster 客户端需要实现 槽位路由表:
# Python redis-py-cluster 实现
class ClusterNode:
def __init__(self, host, port, node_id):
self.host = host
self.port = port
self.node_id = node_id
self.slots = set() # 节点负责的槽位
class RedisCluster:
def __init__(self, startup_nodes):
self.nodes = []
self.slot_table = [None] * 16384 # 槽位到节点的映射
self.initialize(startup_nodes)
def initialize(self, startup_nodes):
"""初始化集群连接和槽位表"""
# 1. 连接到任意节点
node = startup_nodes[0]
client = redis.StrictRedis(host=node['host'], port=node['port'])
# 2. 获取集群节点信息
cluster_nodes = client.execute_command('CLUSTER', 'NODES')
# 3. 解析节点信息并建立连接
for line in cluster_nodes.split('\n'):
parts = line.split()
node_id, addr, flags, master_id, ping_sent, \
pong_recv, config_epoch, link_state, slots = parts[:9]
host, port = addr.split('@')[0].split(':')
cluster_node = ClusterNode(host, port, node_id)
self.nodes.append(cluster_node)
# 4. 解析槽位范围
for slot_range in slots:
if '-' in slot_range:
start, end = map(int, slot_range.split('-'))
for slot in range(start, end + 1):
self.slot_table[slot] = cluster_node
else:
slot = int(slot_range)
self.slot_table[slot] = cluster_node
def get_node_by_slot(self, slot):
"""根据槽位获取目标节点"""
return self.slot_table[slot]
def execute_command(self, *args):
"""执行命令并处理重定向"""
key = self.extract_key(args)
slot = self.calculate_slot(key)
node = self.get_node_by_slot(slot)
client = redis.StrictRedis(host=node.host, port=node.port)
try:
return client.execute_command(*args)
except redis.ResponseError as e:
# 处理 MOVED 重定向
if str(e).startswith('MOVED'):
_, slot, target_addr = str(e).split()
host, port = target_addr.split(':')
self.update_slot_cache(int(slot), host, int(port))
return self.execute_command(*args) # 重试
raise
def calculate_slot(self, key):
"""CRC16 计算槽位"""
import crc16
return crc16.crc16(key.encode()) % 16384
5.2 重定向处理
MOVED vs ASK:
| 类型 | 触发条件 | 行为 |
|---|---|---|
| MOVED | 槽位已永久迁移到其他节点 | 更新本地缓存,后续请求直接发送到新节点 |
| ASK | 槽位正在迁移中,键可能在新旧节点 | 仅本次请求重定向,不更新缓存 |
def handle_redirection(self, error):
"""处理重定向错误"""
error_str = str(error)
if error_str.startswith('MOVED'):
# 槽位永久迁移
_, slot, target = error_str.split()
slot = int(slot)
host, port = target.split(':')
# 更新缓存
self.slot_table[slot] = self.find_or_create_node(host, int(port))
# 重试
return self.retry_with_new_node(slot)
elif error_str.startswith('ASK'):
# 槽位迁移中
_, slot, target = error_str.split()
host, port = target.split(':')
# 发送 ASKING 命令
target_node = self.get_node(host, int(port))
target_node.execute_command('ASKING')
# 重试(不更新缓存)
return target_node.execute_command(*self.last_command)
5.3 客户端实现对比
| 客户端 | 语言 | Smart Client | 支持特性 | 生产就绪 |
|---|---|---|---|---|
| redis-py-cluster | Python | ✅ | Pipeline, Pub/Sub | ✅ |
| Jedis | Java | ✅ | Pipeline, Transaction, Lua | ✅ |
| go-redis | Go | ✅ | Pipeline, Pub/Sub, Sentinel | ✅ |
| node-redis-cluster | Node.js | ✅ | Pipeline, Promise API | ✅ |
| phpredis | PHP | ✅ | Pipeline, Multi/Exec | ✅ |
六、高级特性与优化
6.1 集群总线(Cluster Bus)
Redis Cluster 使用两个 TCP 连接:
- 客户端端口:如 6379,用于接收客户端命令
- 集群总线端口:客户端端口 + 10000(如 16379),用于节点间通信
// cluster.c (Redis 7.2.0) - 创建集群总线
int clusterBusCreateSocket(void) {
int bus_fd = anetTcpServer(
server.neterr,
server.port + CLUSTER_PORT_INCR, /* 10000 */
server.bindaddr,
AF_UNSPEC
);
anetNonBlock(NULL, bus_fd);
anetEnableTcpNoDelay(NULL, bus_fd);
return bus_fd;
}
void clusterSendPing(clusterNode *node) {
/* 通过集群总线发送 PING */
clusterMsg *msg = createClusterMessage(CLUSTERMSG_TYPE_PING);
/* 包含节点自己的槽位分配信息 */
for (int i = 0; i < CLUSTER_SLOTS; i++) {
if (server.cluster->myslots[i] != NULL) {
msg->data.ping.slots[i] = myself->name;
}
}
clusterSendMessage(node, msg, CLUSTER_MSG_TYPE_PING);
}
6.2 Gossip 消息类型
| 消息类型 | 值 | 用途 |
|---|---|---|
MEET |
1 | 新节点加入集群 |
PING |
2 | 节点心跳检测 |
PONG |
3 | PING 响应 |
FAIL |
4 | 宣布节点故障 |
PUBLISH |
5 | 跨节点 Pub/Sub |
FAILOVER_AUTH_REQUEST |
10 | 请求投票 |
FAILOVER_AUTH_ACK |
11 | 投票响应 |
6.3 性能优化建议
| 优化项 | 配置/方法 | 效果 |
|---|---|---|
| 禁用 THP | echo never > /sys/kernel/mm/transparent_hugepage/enabled |
避免内存延迟分配 |
| 调整哈希表大小 | hash-max-ziplist-entries 512 |
减少内存碎片 |
| 使用 Pipeline | 客户端批量发送命令 | 减少 RTT |
| 连接池 | 复用连接 | 减少 TCP 握手开销 |
| 避免热点键 | 使用 Hash Tags 均衡负载 | 避免单点压力 |
| 禁用 swap | swapoff -a |
避免性能抖动 |
监控指标:
# 槽位分配情况
redis-cli -p 7000 cluster slots | jq '.'
# 节点信息
redis-cli -p 7000 cluster nodes | awk '{print $2, $3, $8, $9}'
# 槽位分布
redis-cli -p 7000 --cluster check 127.0.0.1:7000
# 关键指标
redis-cli -p 7000 info stats | grep cluster
# cluster_connections: 12
# cluster_stats_messages_sent: 15234
# cluster_stats_messages_received: 14856
七、故障排查指南
7.1 常见问题诊断
问题 1:槽位未完全分配
# 症状
redis-cli -c -p 7000 SET key1 value1
(error) CLUSTERDOWN Hash slot not served
# 诊断
redis-cli -p 7000 cluster info
cluster_state:fail # 集群状态异常
cluster_slots_assigned: 8192 # 只分配了一半槽位
# 解决:分配剩余槽位
redis-cli --cluster fix 127.0.0.1:7000 --cluster-yes
问题 2:节点脑裂
# 症状:多个节点认为自己是 Master
redis-cli -p 7000 cluster nodes | grep master
53e0... myself,master - 0 0 connected 0-5460
92f1... master - 0 0 connected 0-5460 # 重复!
# 原因:网络分区
# 解决:手动合并或重置集群
redis-cli -p 7000 cluster reset # 在从节点执行
问题 3:从节点无法晋升
# 症状:Master 故障后从节点未接管
redis-cli -p 7000 cluster info
cluster_state:ok
cluster_slots_ok: 10922 # 槽位减少
# 诊断
redis-cli -p 7001 cluster info
cluster_repl_validity_factor: 10 # 从节点断连时间过长
# 解决:调小 validity-factor
redis-cli -p 7001 CONFIG SET cluster-replica-validity-factor 0
7.2 集群状态检查清单
#!/bin/bash
# redis-cluster-check.sh
echo "=== Redis Cluster 健康检查 ==="
# 1. 检查集群状态
echo -e "\n[1] 集群状态"
redis-cli -p 7000 cluster info | grep -E "cluster_state|cluster_slots_ok"
# 2. 检查槽位覆盖
echo -e "\n[2] 槽位分配"
redis-cli -p 7000 cluster slots | jq '[.[] | {range: .[0:2], node: .[2][0]}]'
# 3. 检查复制关系
echo -e "\n[3] 主从关系"
redis-cli -p 7000 cluster replicas $(redis-cli -p 7000 cluster myid)
# 4. 检查节点健康
echo -e "\n[4] 节点状态"
for port in 7000 7001 7002 7003 7004 7005; do
echo "Node $port:"
redis-cli -p $port ping
done
# 5. 检查槽位迁移
echo -e "\n[5] 槽位迁移状态"
redis-cli -p 7000 cluster info | grep cluster_slots_assigned
echo -e "\n=== 检查完成 ==="
八、总结
Redis Cluster 通过 16384 个哈希槽实现数据分片,通过 Gossip 协议和 Raft-like 选举实现故障转移,构建了一个去中心化、高可用、可水平扩展的分布式缓存系统。
核心要点:
- 数据分片:基于 CRC16 算法将键映射到 16384 个槽位
- 故障检测:通过 PFAIL → FAIL 状态机,超过半数 Master 确认
- 自动故障转移:从节点通过选举晋升为 Master
- Smart Client:客户端维护槽位缓存,处理 MOVED/ASK 重定向
- 去中心化:所有节点通过 Gossip 协议交换状态
适用场景:
- ✅ 数据量超过单机内存(> 50GB)
- ✅ 需要高可用和自动故障转移
- ✅ 对写入性能有较高要求
- ❌ 需要跨slot事务或多键操作(需使用 Hash Tags)
- ❌ 对延迟极其敏感(集群会增加跳数)
参考资料
版权声明: 本文为原创技术文章,转载请注明出处。
作者: [你的昵称]
发布时间: 2026-04-01
Redis 版本: 7.2.0
更多推荐



所有评论(0)