Redis Cluster集群:分片存储,支撑海量数据与高并发

1. Redis Cluster概述

1.1 什么是Redis Cluster

Redis Cluster是Redis官方提供的分布式解决方案,通过数据分片多主节点架构,实现了Redis在水平方向的扩展能力。它可以支撑海量数据存储高并发访问

1.2 Cluster的核心特性

特性 描述 优势
数据分片 自动将数据分散到多个节点 支撑海量数据
多主架构 多个主节点同时提供服务 高并发处理
自动故障转移 主节点故障时自动切换 高可用保障
水平扩展 支持动态添加删除节点 弹性伸缩

1.3 Cluster与其他方案对比

方案 数据分布 故障转移 扩展性 复杂度
单机Redis 单节点 简单
主从+哨兵 主从复制 自动 读扩展 中等
Redis Cluster 分片存储 自动 读写扩展 复杂

2. 集群核心原理

2.1 哈希槽机制

Redis Cluster使用16384个哈希槽来分配数据:

客户端请求
计算key的CRC16值
CRC16 mod 16384
得到哈希槽编号
路由到对应节点
节点A: 0-5460
5461个槽
节点B: 5461-10922
5461个槽
节点C: 10923-16383
5460个槽

2.2 集群拓扑

通信
通信
通信
通信
通信
通信
Master-1
Slave-1
Master-2
Slave-2
Master-3
Slave-3

2.3 节点发现机制

2.3.1 Gossip协议

Redis Cluster使用Gossip协议进行节点间通信:

  • PING消息:检测节点存活状态
  • PONG消息:响应PING消息
  • MEET消息:通知新节点加入
  • FAIL消息:标记节点故障
2.3.2 集群状态同步
# 每个节点维护集群状态信息
127.0.0.1:7000> CLUSTER NODES
07c37dfeb235213a872192d90877d0cd55635b91 192.168.1.30:7000@17000 slave e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca 0 1557296957000 6 connected
67ed2db8d677e59ec4a4cefb06858cf2a1a89fa1 192.168.1.30:7002@17002 master - 0 1557296956000 8 connected 10923-16383

3. 集群部署配置

3.1 环境规划

3.1.1 节点规划
节点 IP地址 端口 角色 哈希槽范围
redis-1 192.168.1.10 7000 Master 0-5460
redis-2 192.168.1.11 7000 Master 5461-10922
redis-3 192.168.1.12 7000 Master 10923-16383
redis-4 192.168.1.10 7001 Slave -
redis-5 192.168.1.11 7001 Slave -
redis-6 192.168.1.12 7001 Slave -

3.2 节点配置文件

# redis-7000.conf
port 7000
bind 192.168.1.10
daemonize yes
pidfile /var/run/redis/redis-7000.pid
logfile /var/log/redis/redis-7000.log
dir /var/lib/redis/7000

# 集群配置
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-node-timeout 15000
cluster-announce-ip 192.168.1.10
cluster-announce-port 7000
cluster-announce-bus-port 17000

# 持久化配置
appendonly yes
appendfsync everysec

# 安全配置
requirepass cluster123456
masterauth cluster123456

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

3.3 自动化部署脚本

#!/bin/bash
# deploy-cluster.sh

NODES=("192.168.1.10" "192.168.1.11" "192.168.1.12")
PORTS=(7000 7001)

# 部署单个节点
deploy_node() {
    local host=$1
    local port=$2
    
    echo "在 $host:$port 上部署Redis节点..."
    
    # 创建配置文件
    cat > redis-${port}.conf << EOF
port ${port}
bind ${host}
daemonize yes
pidfile /var/run/redis/redis-${port}.pid
logfile /var/log/redis/redis-${port}.log
dir /var/lib/redis/${port}

cluster-enabled yes
cluster-config-file nodes-${port}.conf
cluster-node-timeout 15000
cluster-announce-ip ${host}
cluster-announce-port ${port}
cluster-announce-bus-port $((port + 10000))

appendonly yes
appendfsync everysec
requirepass cluster123456
masterauth cluster123456

maxmemory 2gb
maxmemory-policy allkeys-lru
EOF
    
    # 复制配置到目标服务器
    scp redis-${port}.conf root@${host}:/etc/redis/
    
    # 创建数据目录
    ssh root@${host} "mkdir -p /var/lib/redis/${port}"
    
    # 启动Redis实例
    ssh root@${host} "redis-server /etc/redis/redis-${port}.conf"
    
    echo "节点 $host:$port 部署完成"
}

# 部署所有节点
for host in "${NODES[@]}"; do
    for port in "${PORTS[@]}"; do
        deploy_node $host $port
    done
done

# 等待节点启动
echo "等待节点启动..."
sleep 10

# 创建集群
echo "创建Redis集群..."
redis-cli --cluster create \
    192.168.1.10:7000 192.168.1.11:7000 192.168.1.12:7000 \
    192.168.1.10:7001 192.168.1.11:7001 192.168.1.12:7001 \
    --cluster-replicas 1 \
    -a cluster123456

echo "集群部署完成!"

3.4 集群初始化

# 创建集群
redis-cli --cluster create \
  192.168.1.10:7000 192.168.1.11:7000 192.168.1.12:7000 \
  192.168.1.10:7001 192.168.1.11:7001 192.168.1.12:7001 \
  --cluster-replicas 1 -a cluster123456

# 检查集群状态
redis-cli --cluster check 192.168.1.10:7000 -a cluster123456

# 查看集群信息  
redis-cli -c -h 192.168.1.10 -p 7000 -a cluster123456 cluster info

4. 数据分片机制

4.1 哈希槽分配算法

public class RedisClusterSlotCalculator {
    
    private static final int CLUSTER_SLOTS = 16384;
    
    /**
     * 计算key对应的哈希槽
     */
    public static int calculateSlot(String key) {
        // 检查是否有哈希标签
        int start = key.indexOf('{');
        int end = key.indexOf('}', start + 1);
        
        String hashKey;
        if (start != -1 && end != -1 && end > start + 1) {
            // 使用哈希标签内的内容计算
            hashKey = key.substring(start + 1, end);
        } else {
            // 使用完整key计算
            hashKey = key;
        }
        
        return crc16(hashKey.getBytes()) % CLUSTER_SLOTS;
    }
    
    /**
     * CRC16算法实现
     */
    private static int crc16(byte[] bytes) {
        int crc = 0x0000;
        int polynomial = 0x1021;
        
        for (byte b : bytes) {
            for (int i = 0; i < 8; i++) {
                boolean bit = ((b >> (7 - i) & 1) == 1);
                boolean c15 = ((crc >> 15 & 1) == 1);
                crc <<= 1;
                if (c15 ^ bit) {
                    crc ^= polynomial;
                }
            }
        }
        
        return crc & 0xFFFF;
    }
}

4.2 哈希标签使用

@Service
public class ClusterKeyDesignService {
    
    /**
     * 使用哈希标签确保相关数据在同一节点
     */
    public void demonstrateHashTags() {
        // 用户相关的所有数据使用相同标签
        String userId = "1001";
        
        // 这些key会分配到同一个哈希槽
        String profileKey = "user:{" + userId + "}:profile";
        String settingsKey = "user:{" + userId + "}:settings";
        String preferencesKey = "user:{" + userId + "}:preferences";
        
        System.out.println("使用哈希标签的key分布:");
        System.out.println(profileKey + " -> slot: " + 
            RedisClusterSlotCalculator.calculateSlot(profileKey));
        System.out.println(settingsKey + " -> slot: " + 
            RedisClusterSlotCalculator.calculateSlot(settingsKey));
        System.out.println(preferencesKey + " -> slot: " + 
            RedisClusterSlotCalculator.calculateSlot(preferencesKey));
    }
}

5. 故障处理与扩容

5.1 故障检测机制

节点A发送PING
节点B
是否响应PONG
标记节点正常
标记节点疑似故障
询问其他节点
多数节点确认故障
标记节点故障
继续监控
触发故障转移

5.2 集群扩容

#!/bin/bash
# add-node.sh

NEW_NODE_HOST="192.168.1.13"
NEW_NODE_PORT="7000"
EXISTING_NODE="192.168.1.10:7000"
PASSWORD="cluster123456"

echo "添加新主节点..."

# 启动新节点
ssh root@${NEW_NODE_HOST} "redis-server /etc/redis/redis-${NEW_NODE_PORT}.conf"

# 添加节点到集群
redis-cli --cluster add-node \
    ${NEW_NODE_HOST}:${NEW_NODE_PORT} \
    ${EXISTING_NODE} \
    -a ${PASSWORD}

# 重新分配哈希槽
echo "重新分配哈希槽..."
redis-cli --cluster reshard ${EXISTING_NODE} \
    --cluster-from all \
    --cluster-to $(redis-cli -h ${NEW_NODE_HOST} -p ${NEW_NODE_PORT} -a ${PASSWORD} cluster myid) \
    --cluster-slots 4096 \
    --cluster-yes \
    -a ${PASSWORD}

echo "集群扩容完成!"

6. Java应用集成

6.1 Lettuce集群配置

@Configuration
public class RedisClusterConfig {
    
    @Value("${spring.redis.cluster.nodes}")
    private String clusterNodes;
    
    @Value("${spring.redis.password}")
    private String password;
    
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        // 解析集群节点
        List<String> nodes = Arrays.asList(clusterNodes.split(","));
        Set<RedisNode> redisNodes = nodes.stream()
            .map(node -> {
                String[] hostPort = node.trim().split(":");
                return new RedisNode(hostPort[0], Integer.parseInt(hostPort[1]));
            })
            .collect(Collectors.toSet());
        
        RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
        clusterConfig.setClusterNodes(redisNodes);
        clusterConfig.setPassword(password);
        clusterConfig.setMaxRedirects(3);
        
        return new LettuceConnectionFactory(clusterConfig);
    }
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());
        
        // 序列化配置
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        
        template.afterPropertiesSet();
        return template;
    }
}

6.2 集群操作服务

@Service
public class ClusterOperationService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 使用哈希标签的批量操作
     */
    public void batchOperationWithHashTag(String userId, Map<String, Object> userData) {
        // 使用哈希标签确保所有操作在同一节点
        String profileKey = "user:{" + userId + "}:profile";
        String settingsKey = "user:{" + userId + "}:settings";
        String preferencesKey = "user:{" + userId + "}:preferences";
        
        // 可以使用事务,因为都在同一节点
        redisTemplate.multi();
        redisTemplate.opsForHash().putAll(profileKey, userData);
        redisTemplate.opsForValue().set(settingsKey, userData.get("settings"));
        redisTemplate.opsForValue().set(preferencesKey, userData.get("preferences"));
        redisTemplate.exec();
    }
    
    /**
     * 跨节点数据收集
     */
    public Map<String, Object> collectDataFromAllNodes(List<String> keys) {
        Map<String, Object> result = new HashMap<>();
        
        for (String key : keys) {
            try {
                Object value = redisTemplate.opsForValue().get(key);
                if (value != null) {
                    result.put(key, value);
                }
            } catch (Exception e) {
                // 处理节点故障或网络问题
                result.put(key, "ERROR: " + e.getMessage());
            }
        }
        
        return result;
    }
}

7. 运维监控

7.1 集群监控服务

@Service
public class ClusterMonitoringService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 获取集群整体状态
     */
    public ClusterStatus getClusterStatus() {
        return redisTemplate.execute((RedisCallback<ClusterStatus>) connection -> {
            Properties clusterInfo = connection.clusterInfo();
            
            ClusterStatus status = new ClusterStatus();
            status.setState(clusterInfo.getProperty("cluster_state"));
            status.setSlotsAssigned(Integer.parseInt(clusterInfo.getProperty("cluster_slots_assigned", "0")));
            status.setSlotsOk(Integer.parseInt(clusterInfo.getProperty("cluster_slots_ok", "0")));
            status.setKnownNodes(Integer.parseInt(clusterInfo.getProperty("cluster_known_nodes", "0")));
            
            return status;
        });
    }
    
    /**
     * 健康检查
     */
    @Scheduled(fixedRate = 30000)
    public void performHealthCheck() {
        try {
            ClusterStatus status = getClusterStatus();
            
            if (!"ok".equals(status.getState())) {
                sendAlert("集群状态异常", "当前状态: " + status.getState());
            }
            
            if (status.getSlotsAssigned() != 16384) {
                sendAlert("哈希槽分配不完整", 
                    "已分配槽数: " + status.getSlotsAssigned() + "/16384");
            }
            
        } catch (Exception e) {
            sendAlert("健康检查失败", e.getMessage());
        }
    }
    
    private void sendAlert(String title, String message) {
        System.err.println("CLUSTER ALERT: " + title + " - " + message);
    }
}

8. 最佳实践

8.1 架构设计建议

8.1.1 节点数量规划
  • 最少6个节点:3主3从保证高可用
  • 奇数个主节点:便于选举决策
  • 每主配一从:保证故障转移能力
8.1.2 硬件配置
角色 CPU 内存 存储 网络
主节点 8核+ 16GB+ SSD 千兆+
从节点 4核+ 8GB+ SSD 千兆+

8.2 性能优化

8.2.1 合理使用哈希标签
// ✅ 推荐:相关数据使用相同标签
String userPrefix = "{user:" + userId + "}";
String profileKey = userPrefix + ":profile";
String settingsKey = userPrefix + ":settings";

// ❌ 避免:无关数据使用相同标签
String mixedKey = "{common}:user:1001:profile"; // 会导致数据倾斜
8.2.2 避免大key和热key
@Service
public class KeyOptimizationService {
    
    /**
     * 拆分大key
     */
    public void splitLargeHash(String largeKey, Map<String, String> data) {
        // 将大hash拆分为多个小hash
        int chunkSize = 1000;
        int chunkIndex = 0;
        
        Map<String, String> chunk = new HashMap<>();
        for (Map.Entry<String, String> entry : data.entrySet()) {
            chunk.put(entry.getKey(), entry.getValue());
            
            if (chunk.size() >= chunkSize) {
                String chunkKey = largeKey + ":chunk:" + chunkIndex++;
                redisTemplate.opsForHash().putAll(chunkKey, chunk);
                chunk.clear();
            }
        }
        
        // 处理剩余数据
        if (!chunk.isEmpty()) {
            String chunkKey = largeKey + ":chunk:" + chunkIndex;
            redisTemplate.opsForHash().putAll(chunkKey, chunk);
        }
    }
}

8.3 运维建议

8.3.1 监控关键指标
  • 集群状态:cluster_state必须为ok
  • 哈希槽分配:16384个槽全部分配
  • 节点通信:cluster_known_nodes数量正确
  • 内存使用:各节点内存使用均衡
8.3.2 容灾规划
#!/bin/bash
# cluster-backup.sh

CLUSTER_NODES=("192.168.1.10:7000" "192.168.1.11:7000" "192.168.1.12:7000")
BACKUP_DIR="/backup/redis-cluster/$(date +%Y%m%d)"

# 创建备份目录
mkdir -p ${BACKUP_DIR}

# 备份每个主节点
for node in "${CLUSTER_NODES[@]}"; do
    host=$(echo $node | cut -d: -f1)
    port=$(echo $node | cut -d: -f2)
    
    echo "备份节点 $node..."
    redis-cli -h $host -p $port -a cluster123456 BGSAVE
    
    # 等待备份完成
    sleep 10
    
    # 复制RDB文件
    scp root@${host}:/var/lib/redis/${port}/dump.rdb \
        ${BACKUP_DIR}/dump-${host}-${port}.rdb
done

echo "集群备份完成!"

总结

Redis Cluster提供了完整的分布式Redis解决方案:

核心知识点

  1. 数据分片:通过16384个哈希槽实现数据自动分片
  2. 多主架构:多个主节点同时提供读写服务
  3. 自动故障转移:主节点故障时从节点自动接管
  4. 水平扩展:支持动态添加删除节点
  5. Java集成:完善的Spring Boot集成支持

关键要点

  • 哈希槽机制:数据分片的核心,支持一致性哈希
  • 节点通信:基于Gossip协议的集群状态同步
  • 故障检测:分布式故障检测和自动恢复
  • 客户端路由:智能路由和重定向机制

最佳实践

  1. 合理规划架构:至少6节点,主从配对部署
  2. 优化数据分布:使用哈希标签,避免数据倾斜
  3. 监控集群状态:实时监控节点和哈希槽状态
  4. 制定运维规范:建立完善的备份和恢复流程
  5. 性能调优:避免大key和热key问题

Redis Cluster是构建大规模Redis架构的最佳选择,为海量数据和高并发提供了可靠的解决方案。


下一篇预告《Redis性能优化实战:从原理到实践,让Redis跑更快》


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐