跳到主要内容

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
数据分片
高可用手动切换自动切换自动切换
扩展方式只读扩展只读扩展读写扩展
配置复杂度
最少节点23(哨兵)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
...

小结

本章我们学习了:

  1. 集群架构:分片原理、哈希槽分配
  2. 集群配置:配置文件、端口说明
  3. 集群创建:手动创建、脚本创建、Docker 部署
  4. 集群操作:连接、状态查看、节点管理
  5. 故障转移:自动转移、手动转移
  6. 客户端连接:Python、Java、Node.js 示例
  7. 限制与最佳实践:多键操作、事务、Lua 脚本限制

练习

  1. 搭建一个 6 节点的 Redis Cluster
  2. 使用哈希标签实现用户信息的多键操作
  3. 测试故障转移功能
  4. 编写 Python 程序连接 Cluster 并执行操作

参考资源