Redis Cluster 集群
Redis Cluster 是 Redis 的分布式实现,提供自动数据分片、高可用和水平扩展能力。理解 Cluster 的工作原理对于构建大规模 Redis 应用至关重要。
集群概述
什么是 Redis Cluster?
Redis Cluster 将数据自动分片到多个节点,并提供一定程度的可用性保证。它不是使用一致性哈希,而是使用一种称为"哈希槽"的不同分片方式。
核心能力:
- 自动分片:数据自动分布到多个节点,无需手动分区
- 高可用:当部分节点故障时,集群仍可继续运行
- 水平扩展:支持动态添加或删除节点,无需停机
Cluster 的工作原理
Redis Cluster 不使用传统的代理方式,而是采用去中心化的设计。每个节点都保存着集群的完整状态信息,客户端可以直接连接到正确的节点。
Cluster 与其他方案对比
| 特性 | 主从复制 | 哨兵模式 | Cluster |
|---|---|---|---|
| 数据分片 | 不支持 | 不支持 | 支持 |
| 高可用 | 手动切换 | 自动切换 | 自动切换 |
| 扩展方式 | 只读扩展 | 只读扩展 | 读写扩展 |
| 配置复杂度 | 低 | 中 | 高 |
| 最少节点数 | 2 | 3(哨兵) | 6 |
| 适用场景 | 读多写少 | 中等规模 | 大规模、高并发 |
分片原理
哈希槽机制
Redis Cluster 使用哈希槽进行数据分片,这是理解 Cluster 的关键:
- 共有 16384 个哈希槽
- 每个键通过
CRC16(key) % 16384计算所属槽位 - 槽位均匀分配给各 Master 节点
为什么是 16384 个槽?
这个数字是经过精心选择的权衡结果:
- 槽数量足够大,可以在数千个节点间均匀分布
- 槽数量又不是太大,使得槽位映射表可以在节点间高效传播
- CRC16 可以产生 65536 个值,16384 正好是其 1/4,计算高效
槽位分配示例:
假设有 3 个 Master 节点,槽位分配如下:
| 节点 | 槽位范围 | 槽位数量 |
|---|---|---|
| Master A | 0 - 5460 | 5461 |
| Master B | 5461 - 10922 | 5462 |
| Master C | 10923 - 16383 | 5461 |
键的槽位计算
# 手动计算键的槽位
# 公式: CRC16(key) % 16384
# 示例:计算 "user:1" 的槽位
127.0.0.1:7000> CLUSTER KEYSLOT user:1
(integer) 5474
# 这个槽位在 Master B 的范围内
哈希标签(Hash Tags)
哈希标签是确保多个键分配到同一槽位的机制。只有 {} 括号内的内容参与哈希计算。
# 这三个键会被分配到同一个槽位
user:{123}:profile # 槽位由 "123" 决定
user:{123}:settings # 同一槽位
user:{123}:orders # 同一槽位
# 实际操作示例
127.0.0.1:7000> SET user:{1001}:name "张三"
OK
127.0.0.1:7000> SET user:{1001}:age "25"
OK
# 可以在同一条命令中操作多个键
127.0.0.1:7000> MGET user:{1001}:name user:{1001}:age
1) "张三"
2) "25"
# 不使用哈希标签,多键操作会失败
127.0.0.1:7000> MGET user:a:name user:b:age
(error) CROSSSLOT Keys in request don't hash to the same slot
哈希标签的设计建议:
# 好的设计:相关数据使用相同的哈希标签
user:{user_id}:profile # 用户资料
user:{user_id}:settings # 用户设置
user:{user_id}:orders # 用户订单
# 避免:不同实体使用不同标签导致分散
user:profile:{user_id} # 不同的槽位
user:settings:{user_id} # 不同的槽位
TCP 端口要求
每个 Cluster 节点需要两个 TCP 端口:
| 端口类型 | 默认值 | 用途 |
|---|---|---|
| 客户端端口 | 6379 | 客户端连接、节点间数据迁移 |
| 集群总线端口 | 16379 | 节点间通信、故障检测、配置更新 |
重要说明:
- 集群总线端口 = 客户端端口 + 10000
- 两个端口都必须在防火墙中开放
- 客户端不应尝试连接集群总线端口
集群配置
配置文件详解
每个节点的 redis.conf 需要以下配置:
# 基础配置
port 7000
daemonize yes
appendonly yes
# 集群配置
cluster-enabled yes # 启用集群模式
cluster-config-file nodes-7000.conf # 集群配置文件(自动维护)
cluster-node-timeout 5000 # 节点超时时间(毫秒)
cluster-announce-ip 192.168.1.100 # 外部访问 IP
cluster-announce-port 7000 # 外部访问端口
cluster-announce-bus-port 17000 # 集群总线端口
配置参数说明
| 参数 | 说明 | 默认值 |
|---|---|---|
cluster-enabled | 是否启用集群模式 | no |
cluster-config-file | 集群状态持久化文件 | nodes.conf |
cluster-node-timeout | 节点不可用判定时间 | 15000ms |
cluster-slave-validity-factor | 从节点有效性因子 | 10 |
cluster-migration-barrier | 主从迁移屏障 | 1 |
cluster-require-full-coverage | 是否要求槽位全覆盖 | yes |
cluster-allow-reads-when-down | 集群不可用时是否允许读 | no |
最小集群配置
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
创建集群
方法一:手动创建
步骤一:准备目录和配置
# 创建集群目录
mkdir -p cluster/{7000,7001,7002,7003,7004,7005}
# 为每个节点创建配置文件
for port in 7000 7001 7002 7003 7004 7005; do
cat > cluster/$port/redis.conf << EOF
port $port
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
daemonize yes
logfile $port.log
EOF
done
步骤二:启动所有节点
for port in 7000 7001 7002 7003 7004 7005; do
redis-server cluster/$port/redis.conf
done
步骤三:创建集群
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
输出示例:
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:7004 to 127.0.0.1:7000
Adding replica 127.0.0.1:7005 to 127.0.0.1:7001
Adding replica 127.0.0.1:7003 to 127.0.0.1:7002
...
[OK] All 16384 slots covered.
方法二:使用脚本创建
Redis 提供了便捷的创建脚本:
# 进入 Redis 源码目录
cd /path/to/redis/utils/create-cluster
# 启动并创建集群
./create-cluster start
./create-cluster create
# 停止集群
./create-cluster stop
方法三:Docker Compose 部署
version: '3.8'
services:
redis-7000:
image: redis:7
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7000
ports:
- "7000:7000"
- "17000:17000"
volumes:
- data-7000:/data
redis-7001:
image: redis:7
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7001
ports:
- "7001:7001"
- "17001:17001"
volumes:
- data-7001:/data
redis-7002:
image: redis:7
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7002
ports:
- "7002:7002"
- "17002:17002"
volumes:
- data-7002:/data
redis-7003:
image: redis:7
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7003
ports:
- "7003:7003"
- "17003:17003"
volumes:
- data-7003:/data
redis-7004:
image: redis:7
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7004
ports:
- "7004:7004"
- "17004:17004"
volumes:
- data-7004:/data
redis-7005:
image: redis:7
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes --port 7005
ports:
- "7005:7005"
- "17005:17005"
volumes:
- data-7005:/data
# 集群初始化
cluster-init:
image: redis:7
depends_on:
- redis-7000
- redis-7001
- redis-7002
- redis-7003
- redis-7004
- redis-7005
command: >
redis-cli --cluster create
redis-7000:7000 redis-7001:7001 redis-7002:7002
redis-7003:7003 redis-7004:7004 redis-7005:7005
--cluster-replicas 1 --cluster-yes
volumes:
data-7000:
data-7001:
data-7002:
data-7003:
data-7004:
data-7005:
集群操作
连接集群
# 使用 -c 参数启用集群模式(支持自动重定向)
redis-cli -c -p 7000
# 自动重定向示例
127.0.0.1:7000> SET key1 "value1"
-> Redirected to slot [9189] located at 127.0.0.1:7001
OK
查看集群状态
# 查看集群信息
127.0.0.1:7000> CLUSTER INFO
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 # Master 数量
# 查看节点信息
127.0.0.1:7000> CLUSTER NODES
abc123... 127.0.0.1:7000@17000 myself,master - 0 1609459200 1 connected 0-5460
def456... 127.0.0.1:7001@17001 master - 0 1609459201 2 connected 5461-10922
...
节点 ID 说明
每个节点都有一个唯一的 Node ID,这是一个 40 字符的十六进制字符串:
- 在节点首次启动时生成
- 永远不会改变(除非删除集群配置文件)
- 用于集群内部标识节点
重新分片
将槽位从一个节点迁移到另一个:
# 交互式重新分片
redis-cli --cluster reshard 127.0.0.1:7000
# 非交互式(适合自动化)
redis-cli --cluster reshard 127.0.0.1:7000 \
--cluster-from <源节点ID> \
--cluster-to <目标节点ID> \
--cluster-slots 1000 \
--cluster-yes
节点管理
添加 Master 节点
# 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
添加 Replica 节点
# 添加从节点到指定 Master
redis-cli --cluster add-node \
127.0.0.1:7006 127.0.0.1:7000 \
--cluster-slave \
--cluster-master-id <Master节点ID>
删除节点
# 1. 如果是 Master,先迁移槽位
redis-cli --cluster reshard 127.0.0.1:7000
# 2. 删除节点
redis-cli --cluster del-node 127.0.0.1:7000 <节点ID>
故障转移
自动故障转移流程
当 Master 节点故障时,Cluster 自动将 Replica 提升为 Master:
手动故障转移
在某些维护场景下,可能需要手动触发故障转移:
# 连接到 Replica 节点
redis-cli -p 7003
# 正常故障转移(需要 Master 确认)
127.0.0.1:7003> CLUSTER FAILOVER
OK
# 强制故障转移(不等待 Master 确认)
127.0.0.1:7003> CLUSTER FAILOVER FORCE
OK
# 接管故障(Master 已下线)
127.0.0.1:7003> CLUSTER FAILOVER TAKEOVER
OK
故障转移模式对比
| 模式 | 使用场景 | 特点 |
|---|---|---|
| 默认 | 正常维护 | 安全,需要 Master 配合 |
| FORCE | Master 无响应但未宕机 | 不需要 Master 确认 |
| TAKEOVER | Master 已完全下线 | 直接接管,不等待投票 |
一致性保证
Redis Cluster 不保证强一致性。在某些条件下,可能会丢失已确认的写入。
为什么会丢数据?
原因一:异步复制
客户端 -> 写入 Master B -> 返回 OK -> 复制到 Replica(异步)
↑
如果此时 Master B 宕机
Replica 可能没有收到数据
原因二:网络分区
时间线:
T1: 客户端写入 Master B
T2: 网络分区,Master B 与集群其他节点隔离
T3: 集群选举新的 Master(原 Replica)
T4: 网络恢复,原 Master B 变为 Replica
结果: T1 的写入丢失
提高一致性的方法
# 使用 WAIT 命令等待复制完成
SET key value
WAIT 1 5000 # 等待至少 1 个 Replica 确认,最多等待 5 秒
客户端连接
Python 示例
from redis.cluster import RedisCluster
# 创建集群连接
rc = RedisCluster(
host='127.0.0.1',
port=7000,
decode_responses=True,
max_connections=100,
retry_on_timeout=True
)
# 正常操作
rc.set('key', 'value')
value = rc.get('key')
# 使用哈希标签进行多键操作
rc.mset({
'user:{1001}:name': '张三',
'user:{1001}:age': '25'
})
values = rc.mget(['user:{1001}:name', 'user:{1001}:age'])
Java 示例(Jedis)
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.HostAndPort;
import java.util.HashSet;
import java.util.Set;
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("127.0.0.1", 7000));
nodes.add(new HostAndPort("127.0.0.1", 7001));
nodes.add(new HostAndPort("127.0.0.1", 7002));
JedisCluster cluster = new JedisCluster(nodes);
cluster.set("key", "value");
String value = cluster.get("key");
cluster.close();
Node.js 示例(ioredis)
const Redis = require('ioredis');
const cluster = new Redis.Cluster([
{ host: '127.0.0.1', port: 7000 },
{ host: '127.0.0.1', port: 7001 },
{ host: '127.0.0.1', port: 7002 },
]);
// 正常操作
await cluster.set('key', 'value');
const value = await cluster.get('key');
// 使用哈希标签
await cluster.mset(
'user:{1001}:name', '张三',
'user:{1001}:age', '25'
);
const values = await cluster.mget(
'user:{1001}:name',
'user:{1001}:age'
);
集群限制
多键操作限制
# 不使用哈希标签会失败
127.0.0.1:7000> MSET key1 value1 key2 value2
(error) CROSSSLOT Keys in request don't hash to the same slot
# 解决方案:使用哈希标签
127.0.0.1:7000> MSET {user}:1:name 张三 {user}:1:age 25
OK
事务限制
# 事务中的键必须在同一槽位
127.0.0.1:7000> MULTI
OK
127.0.0.1:7000> SET key1 value1
QUEUED
127.0.0.1:7000> SET key2 value2
QUEUED
127.0.0.1:7000> EXEC
(error) CROSSSLOT Keys in request don't hash to the same slot
Lua 脚本限制
Lua 脚本中访问的所有键必须在同一槽位:
-- 正确:使用哈希标签
local name = redis.call('GET', KEYS[1]) -- user:{1001}:name
local age = redis.call('GET', KEYS[2]) -- user:{1001}:age
return {name, age}
-- 错误:不同槽位
local a = redis.call('GET', 'key1') -- 槽位 A
local b = redis.call('GET', 'key2') -- 槽位 B
运维最佳实践
检查集群健康
# 检查集群
redis-cli --cluster check 127.0.0.1:7000
# 输出
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: abc123... 127.0.0.1:7000
slots:[0-5460] (5461 slots) master
1 additional replica(s)
...
[OK] All 16384 slots covered.
集群备份
# 在每个 Master 节点执行
redis-cli -p 7000 BGSAVE
redis-cli -p 7001 BGSAVE
redis-cli -p 7002 BGSAVE
# 复制 RDB 文件
cp /var/lib/redis/dump.rdb /backup/redis-7000-$(date +%Y%m%d).rdb
监控关键指标
# 查看集群信息
redis-cli -p 7000 CLUSTER INFO
# 关键监控指标
# cluster_state: ok 集群状态
# cluster_slots_assigned: 16384 已分配槽位
# cluster_slots_ok: 16384 正常槽位
# cluster_known_nodes: 6 节点总数
# cluster_size: 3 Master 数量
设计建议
1. 节点数量
- 最少需要 3 个 Master + 3 个 Replica
- 建议奇数个 Master,便于投票决策
2. 数据分布设计
# 好的设计:使用哈希标签
user:{user_id}:profile
user:{user_id}:settings
user:{user_id}:orders
# 避免:分散的设计
user:profile:{user_id}
user:settings:{user_id}
3. 避免热点
# 不好的设计:所有数据在同一标签
data:{common}:key1
data:{common}:key2
# 好的设计:分散数据
data:{user1}:info
data:{user2}:info
小结
本章我们学习了:
- 集群架构:去中心化设计、哈希槽分配机制
- 分片原理:16384 个槽位、CRC16 计算、哈希标签
- 集群配置:端口要求、配置参数详解
- 集群操作:创建、连接、状态查看、重新分片
- 节点管理:添加/删除 Master 和 Replica
- 故障转移:自动转移、手动转移模式
- 一致性保证:异步复制的影响、WAIT 命令
- 限制与最佳实践:多键操作、事务、Lua 脚本限制
练习
- 搭建一个 6 节点的 Redis Cluster
- 使用哈希标签实现用户信息的多键操作
- 测试故障转移功能:手动停止一个 Master 观察
- 编写 Python 程序连接 Cluster 并执行操作
- 练习添加新节点和重新分片操作