Redis Cluster 集群
Redis Cluster 是 Redis 的分布式实现,提供自动数据分片、高可用和水平扩展能力。
集群概述
什么是 Redis Cluster?
Redis Cluster 将数据自动分片到多个节点,并提供一定程度的可用性保证。
┌─────────────────────────────────────────────────────────────┐
│ Redis Cluster 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 客户端请求 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Master A │ │ Master B │ │ Master C │ │
│ │ 槽位 0-5460│ │槽位 5461-10922│ │槽位 10923-16383│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Replica A1 │ │ Replica B1 │ │ Replica C1 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 总计 16384 个槽位,分布在各 Master 节点 │
│ │
└─────────────────────────────────────────────────────────────┘
Cluster 的特点
| 特性 | 说明 |
|---|---|
| 自动分片 | 数据自动分布到多个节点 |
| 高可用 | Master 故障时自动故障转移 |
| 水平扩展 | 支持动态添加/删除节点 |
| 无中心 | 所有节点互联,无单点故障 |
| 客户端路由 | 客户端可直接连接正确的节点 |
Cluster vs 主从复制 vs 哨兵
| 特性 | 主从复制 | 哨兵 | Cluster |
|---|---|---|---|
| 数据分片 | 否 | 否 | 是 |
| 高可用 | 手动切换 | 自动切换 | 自动切换 |
| 扩展方式 | 只读扩展 | 只读扩展 | 读写扩展 |
| 配置复杂度 | 低 | 中 | 高 |
| 最少节点 | 2 | 3(哨兵) | 6 |
分片原理
哈希槽(Hash Slot)
Redis Cluster 使用哈希槽进行数据分片:
- 共有 16384 个哈希槽
- 每个键属于一个特定的槽
- 槽位分配给各个 Master 节点
┌─────────────────────────────────────────────────────────────┐
│ 哈希槽分配示例 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 键名 → CRC16(key) % 16384 → 槽位号 → 所在节点 │
│ │
│ "user:1" → CRC16("user:1") % 16384 = 5474 → Master B │
│ "order:1" → CRC16("order:1") % 16384 = 7341 → Master B │
│ "product:1" → CRC16("product:1") % 16384 = 12561 → Master C│
│ │
│ 槽位分配: │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Master A │ │ Master B │ │ Master C │ │
│ │ 槽位 │ │ 槽位 │ │ 槽位 │ │
│ │ 0 - 5460 │ │ 5461-10922 │ │ 10923-16383 │ │
│ │ (5461个) │ │ (5462个) │ │ (5461个) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
哈希标签(Hash Tag)
确保多个键在同一槽位,支持多键操作:
# 使用 {} 指定哈希标签
# 只有 {} 内的内容参与哈希计算
user:{123}:profile # 槽位由 "123" 决定
user:{123}:settings # 同一槽位
user:{123}:orders # 同一槽位
# 这样可以在同一命令中操作多个键
MGET user:{123}:profile user:{123}:settings
# 示例
127.0.0.1:6379> SET user:{1001}:name "张三"
OK
127.0.0.1:6379> SET user:{1001}:age "25"
OK
127.0.0.1:6379> MGET user:{1001}:name user:{1001}:age
1) "张三"
2) "25"
# 不使用哈希标签,多键操作会失败
127.0.0.1:6379> MGET user:a:name user:b:age
(error) CROSSSLOT Keys in request don't hash to the same slot
集群配置
配置文件
每个节点的 redis.conf 配置:
# 启用集群模式
cluster-enabled yes
# 集群配置文件(自动生成和维护)
cluster-config-file nodes-6379.conf
# 节点超时时间(毫秒)
cluster-node-timeout 5000
# 集群节点 IP(如需外部访问)
cluster-announce-ip 192.168.1.100
cluster-announce-port 6379
cluster-announce-bus-port 16379
# 其他配置
port 6379
daemonize yes
appendonly yes
端口说明
每个 Cluster 节点需要两个端口:
| 端口 | 说明 |
|---|---|
| 6379 | 客户端连接端口 |
| 16379 | 集群总线端口(6379 + 10000) |
集群总线用于节点间通信:故障检测、配置更新、故障转移授权等。
最小配置
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}
# 每个目录创建配置文件 redis.conf
# 只需修改端口号
# 启动各节点
redis-server cluster/7000/redis.conf
redis-server cluster/7001/redis.conf
redis-server cluster/7002/redis.conf
redis-server cluster/7003/redis.conf
redis-server cluster/7004/redis.conf
redis-server cluster/7005/redis.conf
步骤二:创建集群
# 使用 redis-cli 创建集群
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
# --cluster-replicas 1 表示每个 Master 有 1 个 Replica
输出示例:
>>> 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/src/utils/create-cluster
# 启动集群
./create-cluster start
./create-cluster create
# 停止集群
./create-cluster stop
Docker Compose 部署
version: '3.8'
services:
redis-node-1:
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:
- redis-node-1:/data
redis-node-2:
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:
- redis-node-2:/data
redis-node-3:
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:
- redis-node-3:/data
redis-node-4:
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:
- redis-node-4:/data
redis-node-5:
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:
- redis-node-5:/data
redis-node-6:
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:
- redis-node-6:/data
# 集群初始化容器
cluster-init:
image: redis:7
depends_on:
- redis-node-1
- redis-node-2
- redis-node-3
- redis-node-4
- redis-node-5
- redis-node-6
command: >
redis-cli --cluster create
redis-node-1:7000 redis-node-2:7001 redis-node-3:7002
redis-node-4:7003 redis-node-5:7004 redis-node-6:7005
--cluster-replicas 1 --cluster-yes
volumes:
redis-node-1:
redis-node-2:
redis-node-3:
redis-node-4:
redis-node-5:
redis-node-6:
集群操作
连接集群
# 使用 -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
...
# 查看节点信息
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
...
槽位操作
# 查看键所属槽位
127.0.0.1:7000> CLUSTER KEYSLOT mykey
(integer) 14687
# 查看槽位所在节点
127.0.0.1:7000> CLUSTER NODES | grep 14687
重新分片
将槽位从一个节点迁移到另一个:
# 交互式重新分片
redis-cli --cluster reshard 127.0.0.1:7000
# 非交互式
redis-cli --cluster reshard 127.0.0.1:7000 \
--cluster-from <source-node-id> \
--cluster-to <target-node-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-node-id>
删除节点
# 1. 先迁移槽位(如果是 Master)
redis-cli --cluster reshard 127.0.0.1:7000
# 2. 删除节点
redis-cli --cluster del-node 127.0.0.1:7000 <node-id>
故障转移
自动故障转移
当 Master 节点故障时,Cluster 自动将 Replica 提升为 Master:
┌─────────────────────────────────────────────────────────────┐
│ 自动故障转移 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 正常状态: │
│ ┌─────────────┐ │
│ │ Master A │ ←─ 写入 │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Replica A1 │ │
│ └─────────────┘ │
│ │
│ ─────────────────────────────────────────────────────── │
│ │
│ Master A 故障后: │
│ ┌─────────────┐ │
│ │ Master A │ ✗ 故障 │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Replica A1 │ ──> 提升为 Master │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
手动故障转移
在 Replica 上执行手动故障转移:
# 连接到 Replica 节点
redis-cli -p 7003
# 执行故障转移
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
客户端连接
Python 示例
from redis.cluster import RedisCluster
# 创建集群连接
rc = RedisCluster(
host='127.0.0.1',
port=7000,
decode_responses=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.Set;
import java.util.HashSet;
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
-- 错误:不同槽位
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 nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[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
最佳实践
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. 客户端连接池
from redis.cluster import RedisCluster
# 使用连接池
rc = RedisCluster(
host='127.0.0.1',
port=7000,
max_connections=100,
retry_on_timeout=True,
decode_responses=True
)
4. 避免热点
# 避免所有数据集中在少数槽位
# 不好的设计:所有数据在同一标签
data:{common}:key1
data:{common}:key2
...
# 好的设计:分散数据
data:{user1}:info
data:{user2}:info
...
小结
本章我们学习了:
- 集群架构:分片原理、哈希槽分配
- 集群配置:配置文件、端口说明
- 集群创建:手动创建、脚本创建、Docker 部署
- 集群操作:连接、状态查看、节点管理
- 故障转移:自动转移、手动转移
- 客户端连接:Python、Java、Node.js 示例
- 限制与最佳实践:多键操作、事务、Lua 脚本限制
练习
- 搭建一个 6 节点的 Redis Cluster
- 使用哈希标签实现用户信息的多键操作
- 测试故障转移功能
- 编写 Python 程序连接 Cluster 并执行操作