🔗 Redis 集群模式

实现 Redis 水平扩展和高可用性的分布式解决方案

集群模式概述

Redis Cluster 是 Redis 的分布式实现,它将数据分片存储在多个节点上,提供了水平扩展能力和高可用性。集群模式通过一致性哈希算法将数据分布到不同的节点,每个节点负责一部分数据槽(slot),同时支持主从复制和自动故障转移。

Redis 集群架构

分片 1

Slots: 0-5460
🔴 Master 1

192.168.1.101:7001

🟢 Slave 1

192.168.1.104:7004

分片 2

Slots: 5461-10922
🔴 Master 2

192.168.1.102:7002

🟢 Slave 2

192.168.1.105:7005

分片 3

Slots: 10923-16383
🔴 Master 3

192.168.1.103:7003

🟢 Slave 3

192.168.1.106:7006

特点:数据分片、水平扩展、自动故障转移、无中心架构

🔧 集群配置

配置 Redis 集群的详细步骤:

节点配置文件:

redis-7001.conf(主节点配置):

# 基本配置
port 7001
bind 0.0.0.0

# 启用集群模式
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 15000

# 集群要求至少3个主节点
cluster-require-full-coverage no

# 密码配置
requirepass mypassword
masterauth mypassword

# 持久化配置
appendonly yes
appendfilename "appendonly-7001.aof"

# 日志配置
logfile "/var/log/redis/redis-7001.log"
loglevel notice

# 工作目录
dir /var/lib/redis/7001

# 内存配置
maxmemory 1gb
maxmemory-policy allkeys-lru

# 网络配置
tcp-keepalive 300
timeout 0

创建集群配置脚本:

#!/bin/bash
# 创建Redis集群配置脚本

# 创建目录
mkdir -p /var/lib/redis/{7001,7002,7003,7004,7005,7006}
mkdir -p /var/log/redis

# 端口列表
ports=(7001 7002 7003 7004 7005 7006)

# 生成配置文件
for port in "${ports[@]}"; do
    cat > /etc/redis/redis-${port}.conf << EOF
port ${port}
bind 0.0.0.0
cluster-enabled yes
cluster-config-file nodes-${port}.conf
cluster-node-timeout 15000
cluster-require-full-coverage no
requirepass mypassword
masterauth mypassword
appendonly yes
appendfilename "appendonly-${port}.aof"
logfile "/var/log/redis/redis-${port}.log"
loglevel notice
dir /var/lib/redis/${port}
maxmemory 1gb
maxmemory-policy allkeys-lru
tcp-keepalive 300
timeout 0
EOF
done

echo "配置文件生成完成"

启动集群节点:

# 启动所有节点
redis-server /etc/redis/redis-7001.conf
redis-server /etc/redis/redis-7002.conf
redis-server /etc/redis/redis-7003.conf
redis-server /etc/redis/redis-7004.conf
redis-server /etc/redis/redis-7005.conf
redis-server /etc/redis/redis-7006.conf

# 或者使用循环启动
for port in {7001..7006}; do
    redis-server /etc/redis/redis-${port}.conf &
done

创建集群:

# 使用 redis-cli 创建集群
redis-cli --cluster create \
  192.168.1.101:7001 \
  192.168.1.102:7002 \
  192.168.1.103:7003 \
  192.168.1.104:7004 \
  192.168.1.105:7005 \
  192.168.1.106:7006 \
  --cluster-replicas 1 \
  -a mypassword

# 参数说明:
# --cluster-replicas 1: 每个主节点有1个从节点
# -a mypassword: 集群密码

🎯 数据分片机制

Redis 集群使用哈希槽(Hash Slot)进行数据分片:

1. 键名

user:1001

2. CRC16

计算哈希值

3. 取模

% 16384

4. 槽位

Slot 8765

5. 节点

Master 2

💡 哈希槽特点:

  • 总槽数:16384个槽位(0-16383)
  • 分片算法:CRC16(key) % 16384
  • 槽位分配:每个主节点负责一部分槽位
  • 数据迁移:通过槽位迁移实现扩容缩容

哈希标签(Hash Tag):

# 使用哈希标签确保相关键在同一节点
SET user:{1001}:profile "John Doe"
SET user:{1001}:settings "theme:dark"
SET user:{1001}:posts "post1,post2,post3"

# 所有 user:{1001}:* 的键都会在同一个节点
# 因为哈希计算只使用 {1001} 部分
节点 槽位范围 槽位数量 数据比例
Master 1 0 - 5460 5461 33.3%
Master 2 5461 - 10922 5462 33.3%
Master 3 10923 - 16383 5461 33.4%

🛠️ 集群管理命令

Redis 集群的管理和维护命令:

集群信息查看

# 查看集群信息
CLUSTER INFO

# 查看集群节点
CLUSTER NODES

# 查看槽位分配
CLUSTER SLOTS

# 查看特定键的槽位
CLUSTER KEYSLOT user:1001

节点管理

# 添加节点
CLUSTER MEET 192.168.1.107 7007

# 删除节点
CLUSTER FORGET node-id

# 设置从节点
CLUSTER REPLICATE master-node-id

# 故障转移
CLUSTER FAILOVER

槽位管理

# 分配槽位
CLUSTER ADDSLOTS 0 1 2 3 4

# 删除槽位
CLUSTER DELSLOTS 0 1 2 3 4

# 迁移槽位
CLUSTER SETSLOT 100 MIGRATING target-node-id
CLUSTER SETSLOT 100 IMPORTING source-node-id

数据迁移

# 获取槽位中的键
CLUSTER GETKEYSINSLOT 100 10

# 迁移键
MIGRATE 192.168.1.107 7007 key 0 5000

# 批量迁移
MIGRATE 192.168.1.107 7007 "" 0 5000 KEYS key1 key2 key3

集群管理脚本:

#!/bin/bash
# Redis 集群管理脚本

CLUSTER_NODES=("192.168.1.101:7001" "192.168.1.102:7002" "192.168.1.103:7003")
PASSWORD="mypassword"

# 检查集群状态
check_cluster_status() {
    echo "=== 集群状态检查 ==="
    for node in "${CLUSTER_NODES[@]}"; do
        echo "检查节点: $node"
        redis-cli -h ${node%:*} -p ${node#*:} -a $PASSWORD CLUSTER INFO | grep cluster_state
    done
}

# 查看节点信息
show_cluster_nodes() {
    echo "=== 集群节点信息 ==="
    redis-cli -h ${CLUSTER_NODES[0]%:*} -p ${CLUSTER_NODES[0]#*:} -a $PASSWORD CLUSTER NODES
}

# 查看槽位分配
show_slot_allocation() {
    echo "=== 槽位分配信息 ==="
    redis-cli -h ${CLUSTER_NODES[0]%:*} -p ${CLUSTER_NODES[0]#*:} -a $PASSWORD CLUSTER SLOTS
}

# 添加新节点
add_node() {
    local new_node=$1
    local existing_node=${CLUSTER_NODES[0]}
    
    echo "添加节点 $new_node 到集群"
    redis-cli --cluster add-node $new_node $existing_node -a $PASSWORD
}

# 重新分片
reshard_cluster() {
    local existing_node=${CLUSTER_NODES[0]}
    
    echo "开始重新分片"
    redis-cli --cluster reshard $existing_node -a $PASSWORD
}

# 执行函数
case $1 in
    "status")
        check_cluster_status
        ;;
    "nodes")
        show_cluster_nodes
        ;;
    "slots")
        show_slot_allocation
        ;;
    "add")
        add_node $2
        ;;
    "reshard")
        reshard_cluster
        ;;
    *)
        echo "用法: $0 {status|nodes|slots|add |reshard}"
        ;;
esac

📱 客户端连接

应用程序如何连接到 Redis 集群:

Java 客户端示例(Jedis):

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;

import java.util.HashSet;
import java.util.Set;

public class ClusterExample {
    public static void main(String[] args) {
        // 配置集群节点
        Set clusterNodes = new HashSet<>();
        clusterNodes.add(new HostAndPort("192.168.1.101", 7001));
        clusterNodes.add(new HostAndPort("192.168.1.102", 7002));
        clusterNodes.add(new HostAndPort("192.168.1.103", 7003));
        
        // 连接池配置
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(100);
        poolConfig.setMaxIdle(10);
        poolConfig.setMinIdle(5);
        
        // 创建集群连接
        JedisCluster jedisCluster = new JedisCluster(
            clusterNodes,
            2000,           // 连接超时
            2000,           // 读取超时
            5,              // 最大重定向次数
            "mypassword",   // 密码
            poolConfig
        );
        
        try {
            // 使用集群
            jedisCluster.set("user:1001", "John Doe");
            String value = jedisCluster.get("user:1001");
            System.out.println("Value: " + value);
            
            // 使用哈希标签
            jedisCluster.set("user:{1001}:profile", "profile data");
            jedisCluster.set("user:{1001}:settings", "settings data");
            
        } finally {
            jedisCluster.close();
        }
    }
}

Python 客户端示例(redis-py-cluster):

from rediscluster import RedisCluster

# 配置集群节点
startup_nodes = [
    {"host": "192.168.1.101", "port": "7001"},
    {"host": "192.168.1.102", "port": "7002"},
    {"host": "192.168.1.103", "port": "7003"}
]

# 创建集群连接
rc = RedisCluster(
    startup_nodes=startup_nodes,
    password='mypassword',
    decode_responses=True,
    skip_full_coverage_check=True
)

# 使用集群
rc.set('user:1001', 'John Doe')
value = rc.get('user:1001')
print(f'Value: {value}')

# 使用哈希标签
rc.set('user:{1001}:profile', 'profile data')
rc.set('user:{1001}:settings', 'settings data')

# 批量操作(需要在同一槽位)
pipe = rc.pipeline()
pipe.set('user:{1001}:name', 'John')
pipe.set('user:{1001}:age', '30')
pipe.execute()

Spring Boot 配置:

# application.yml
spring:
  redis:
    password: mypassword
    timeout: 2000ms
    cluster:
      nodes:
        - 192.168.1.101:7001
        - 192.168.1.102:7002
        - 192.168.1.103:7003
        - 192.168.1.104:7004
        - 192.168.1.105:7005
        - 192.168.1.106:7006
      max-redirects: 3
    lettuce:
      pool:
        max-active: 100
        max-idle: 10
        min-idle: 5

🔄 集群扩容缩容

动态调整集群规模的操作流程:

集群扩容:

# 1. 启动新节点
redis-server /etc/redis/redis-7007.conf
redis-server /etc/redis/redis-7008.conf

# 2. 添加主节点
redis-cli --cluster add-node 192.168.1.107:7007 192.168.1.101:7001 -a mypassword

# 3. 重新分片(分配槽位给新节点)
redis-cli --cluster reshard 192.168.1.101:7001 -a mypassword
# 按提示输入:
# - 要移动的槽位数量
# - 目标节点ID(新节点)
# - 源节点ID(all 表示从所有节点平均分配)

# 4. 添加从节点
redis-cli --cluster add-node 192.168.1.108:7008 192.168.1.101:7001 --cluster-slave --cluster-master-id  -a mypassword

集群缩容:

# 1. 迁移槽位(将要删除节点的槽位迁移到其他节点)
redis-cli --cluster reshard 192.168.1.101:7001 -a mypassword
# 将所有槽位从目标节点迁移出去

# 2. 删除从节点
redis-cli --cluster del-node 192.168.1.108:7008  -a mypassword

# 3. 删除主节点(确保没有槽位)
redis-cli --cluster del-node 192.168.1.107:7007  -a mypassword

# 4. 停止节点服务
redis-cli -h 192.168.1.107 -p 7007 -a mypassword SHUTDOWN
redis-cli -h 192.168.1.108 -p 7008 -a mypassword SHUTDOWN

⚠️ 扩容缩容注意事项:

  • 数据迁移:槽位迁移过程中会有短暂的性能影响
  • 客户端重定向:客户端需要支持 MOVED 和 ASK 重定向
  • 操作顺序:严格按照先迁移槽位再删除节点的顺序
  • 监控告警:扩容缩容期间加强监控

集群模式优缺点

方面 优点 缺点
扩展性 水平扩展,支持动态扩容 扩容过程复杂,有性能影响
高可用性 自动故障转移,无单点故障 需要至少3个主节点
性能 读写分离,并行处理 跨槽操作性能较差
数据分布 自动分片,负载均衡 数据倾斜可能性
运维复杂度 自动化程度高 配置和维护复杂

🚨 故障排查

集群常见问题的诊断和解决方法:

常见问题:

⚠️ 集群状态异常:

现象:cluster_state:fail

排查:检查节点连通性、槽位分配、节点数量

# 检查集群状态
redis-cli -c -h 192.168.1.101 -p 7001 -a mypassword CLUSTER INFO

# 检查节点状态
redis-cli -c -h 192.168.1.101 -p 7001 -a mypassword CLUSTER NODES

⚠️ 槽位未完全分配:

现象:部分槽位没有分配给任何节点

解决:手动分配缺失的槽位

# 检查槽位分配
redis-cli --cluster check 192.168.1.101:7001 -a mypassword

# 修复槽位分配
redis-cli --cluster fix 192.168.1.101:7001 -a mypassword

⚠️ 数据迁移失败:

现象:槽位迁移过程中断或失败

解决:重置槽位状态,重新迁移

# 重置槽位状态
CLUSTER SETSLOT  STABLE

# 重新分配槽位
redis-cli --cluster reshard 192.168.1.101:7001 -a mypassword

✅ 集群健康检查脚本:

#!/bin/bash
# Redis 集群健康检查脚本

CLUSTER_NODES=("192.168.1.101:7001" "192.168.1.102:7002" "192.168.1.103:7003")
PASSWORD="mypassword"

echo "=== Redis 集群健康检查 ==="

# 检查集群整体状态
echo "1. 检查集群状态"
for node in "${CLUSTER_NODES[@]}"; do
    host=${node%:*}
    port=${node#*:}
    
    state=$(redis-cli -h $host -p $port -a $PASSWORD CLUSTER INFO 2>/dev/null | grep cluster_state | cut -d: -f2 | tr -d '\r')
    if [ "$state" = "ok" ]; then
        echo "✓ $node: 状态正常"
    else
        echo "✗ $node: 状态异常 ($state)"
    fi
done

# 检查槽位分配
echo "\n2. 检查槽位分配"
host=${CLUSTER_NODES[0]%:*}
port=${CLUSTER_NODES[0]#*:}

slots_info=$(redis-cli -h $host -p $port -a $PASSWORD CLUSTER SLOTS 2>/dev/null)
if [ $? -eq 0 ]; then
    echo "✓ 槽位分配正常"
else
    echo "✗ 槽位分配异常"
fi

# 检查节点连通性
echo "\n3. 检查节点连通性"
for node in "${CLUSTER_NODES[@]}"; do
    host=${node%:*}
    port=${node#*:}
    
    if redis-cli -h $host -p $port -a $PASSWORD ping > /dev/null 2>&1; then
        echo "✓ $node: 连接正常"
    else
        echo "✗ $node: 连接失败"
    fi
done

# 检查主从关系
echo "\n4. 检查主从关系"
nodes_info=$(redis-cli -h $host -p $port -a $PASSWORD CLUSTER NODES 2>/dev/null)
master_count=$(echo "$nodes_info" | grep -c "master")
slave_count=$(echo "$nodes_info" | grep -c "slave")

echo "主节点数量: $master_count"
echo "从节点数量: $slave_count"

if [ $master_count -ge 3 ]; then
    echo "✓ 主节点数量充足"
else
    echo "✗ 主节点数量不足"
fi

echo "\n=== 检查完成 ==="

💡 最佳实践:

  • 节点规划:至少3个主节点,每个主节点配置1个从节点
  • 硬件配置:节点分布在不同的物理机器上
  • 网络优化:确保节点间网络延迟低且稳定
  • 监控告警:监控集群状态、槽位分配、节点健康度
  • 数据设计:合理使用哈希标签,避免热点数据
  • 客户端配置:客户端支持集群模式和重定向
  • 备份策略:定期备份集群数据
  • 扩容规划:提前规划扩容策略和时机