跳到主要内容

Redis Cluster 集群

Redis Cluster 是 Redis 的分布式实现,提供自动数据分片、高可用和水平扩展能力。理解 Cluster 的工作原理对于构建大规模 Redis 应用至关重要。

集群概述

什么是 Redis Cluster?

Redis Cluster 将数据自动分片到多个节点,并提供一定程度的可用性保证。它不是使用一致性哈希,而是使用一种称为"哈希槽"的不同分片方式。

核心能力

  • 自动分片:数据自动分布到多个节点,无需手动分区
  • 高可用:当部分节点故障时,集群仍可继续运行
  • 水平扩展:支持动态添加或删除节点,无需停机

Cluster 的工作原理

Redis Cluster 不使用传统的代理方式,而是采用去中心化的设计。每个节点都保存着集群的完整状态信息,客户端可以直接连接到正确的节点。

Cluster 与其他方案对比

特性主从复制哨兵模式Cluster
数据分片不支持不支持支持
高可用手动切换自动切换自动切换
扩展方式只读扩展只读扩展读写扩展
配置复杂度
最少节点数23(哨兵)6
适用场景读多写少中等规模大规模、高并发

分片原理

哈希槽机制

Redis Cluster 使用哈希槽进行数据分片,这是理解 Cluster 的关键:

  • 共有 16384 个哈希槽
  • 每个键通过 CRC16(key) % 16384 计算所属槽位
  • 槽位均匀分配给各 Master 节点

为什么是 16384 个槽?

这个数字是经过精心选择的权衡结果:

  • 槽数量足够大,可以在数千个节点间均匀分布
  • 槽数量又不是太大,使得槽位映射表可以在节点间高效传播
  • CRC16 可以产生 65536 个值,16384 正好是其 1/4,计算高效

槽位分配示例

假设有 3 个 Master 节点,槽位分配如下:

节点槽位范围槽位数量
Master A0 - 54605461
Master B5461 - 109225462
Master C10923 - 163835461

键的槽位计算

# 手动计算键的槽位
# 公式: 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 配合
FORCEMaster 无响应但未宕机不需要 Master 确认
TAKEOVERMaster 已完全下线直接接管,不等待投票

一致性保证

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

小结

本章我们学习了:

  1. 集群架构:去中心化设计、哈希槽分配机制
  2. 分片原理:16384 个槽位、CRC16 计算、哈希标签
  3. 集群配置:端口要求、配置参数详解
  4. 集群操作:创建、连接、状态查看、重新分片
  5. 节点管理:添加/删除 Master 和 Replica
  6. 故障转移:自动转移、手动转移模式
  7. 一致性保证:异步复制的影响、WAIT 命令
  8. 限制与最佳实践:多键操作、事务、Lua 脚本限制

练习

  1. 搭建一个 6 节点的 Redis Cluster
  2. 使用哈希标签实现用户信息的多键操作
  3. 测试故障转移功能:手动停止一个 Master 观察
  4. 编写 Python 程序连接 Cluster 并执行操作
  5. 练习添加新节点和重新分片操作

参考资料