47-Redis集群详解

5 阅读12分钟

Redis集群详解

一、知识概述

在生产环境中,单机 Redis 无法满足高可用和大规模数据处理的需求。Redis 提供了三种集群方案:主从复制、哨兵模式和 Redis Cluster,每种方案适用于不同的场景。

本文将详细介绍这三种集群方案的原理、配置和实战应用。

二、主从复制

2.1 原理介绍

主从复制是最简单的集群方案,通过将主节点的数据复制到从节点,实现读写分离和数据备份。

核心特点

  • 一个主节点,多个从节点
  • 数据单向复制:主 → 从
  • 主节点负责写,从节点负责读
  • 从节点只能读,不能写

复制流程

  1. 从节点连接主节点,发送 SYNC 命令
  2. 主节点执行 BGSAVE,生成 RDB 快照
  3. 主节点将 RDB 发送给从节点
  4. 从节点加载 RDB
  5. 主节点将期间的写命令发送给从节点(增量复制)

2.2 配置方式

方式一:配置文件
# 从节点配置文件 redis.conf

# 指定主节点
replicaof 192.168.1.100 6379

# 主节点密码
masterauth "your_password"

# 从节点只读
replica-read-only yes

# 复制超时时间
repl-timeout 60
方式二:命令行
# 连接 Redis
redis-cli

# 设置主节点
REPLICAOF 192.168.1.100 6379

# 取消主从关系
REPLICAOF NO ONE
Java 客户端配置
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class MasterSlaveDemo {
    public static void main(String[] args) {
        // 主节点连接池
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(100);
        config.setMaxIdle(50);
        
        JedisPool masterPool = new JedisPool(config, "192.168.1.100", 6379);
        JedisPool slavePool = new JedisPool(config, "192.168.1.101", 6379);
        
        // 写操作 → 主节点
        try (Jedis master = masterPool.getResource()) {
            master.set("key1", "value1");
        }
        
        // 读操作 → 从节点
        try (Jedis slave = slavePool.getResource()) {
            String value = slave.get("key1");
            System.out.println(value);
        }
        
        masterPool.close();
        slavePool.close();
    }
}

2.3 复制原理详解

全量复制
// 全量复制流程示例
public class FullSyncDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 查看复制信息
        String info = jedis.info("replication");
        System.out.println(info);
        
        // 关键指标
        // role:slave                    # 角色:从节点
        // master_host:192.168.1.100     # 主节点地址
        // master_port:6379              # 主节点端口
        // master_link_status:up         # 连接状态
        // master_sync_in_progress:0     # 是否正在同步
        // slave_repl_offset:12345       # 复制偏移量
        
        jedis.close();
    }
}
部分复制(增量复制)

从 Redis 2.8 开始支持部分复制,减少全量复制的开销。

核心概念

  • 复制偏移量:主从节点各自维护
  • 复制积压缓冲区:主节点维护的固定大小队列
  • 运行 ID:主节点的唯一标识
# redis.conf 配置

# 复制积压缓冲区大小
repl-backlog-size 1mb

# 缓冲区生存时间
repl-backlog-ttl 3600

部分复制条件

  1. 主节点运行 ID 不变
  2. 从节点偏移量在积压缓冲区范围内

2.4 优缺点分析

优点

  • 配置简单,易于部署
  • 实现读写分离,提高读性能
  • 数据冗余,提供数据备份

缺点

  • 主节点故障需要手动切换
  • 主节点写入能力受限
  • 主节点内存受限

三、哨兵模式

3.1 原理介绍

哨兵模式在主从复制的基础上,增加了自动故障转移功能。

核心功能

  • 监控:检查主从节点是否正常运行
  • 通知:当节点出现问题时通知管理员
  • 自动故障转移:主节点故障时自动选举新主节点

3.2 架构设计

                    +-------------------+
                    |   Sentinel 集群   |
                    |  (至少3个节点)     |
                    +--------+----------+
                             |
             +---------------+---------------+
             |               |               |
        +----+----+    +-----+-----+    +----+----+
        | Master  |    |  Slave1   |    | Slave2  |
        +---------+    +-----------+    +---------+

3.3 配置方式

哨兵配置文件
# sentinel.conf

# 监控主节点(名称 IP 端口 仲裁人数)
sentinel monitor mymaster 192.168.1.100 6379 2

# 主节点密码
sentinel auth-pass mymaster your_password

# 判断主节点下线的超时时间
sentinel down-after-milliseconds mymaster 30000

# 故障转移超时时间
sentinel failover-timeout mymaster 180000

# 同时可以有多少个从节点对新主节点进行同步
sentinel parallel-syncs mymaster 1

# 哨兵端口
port 26379

# 工作目录
dir /tmp
启动哨兵
# 方式一:使用配置文件启动
redis-sentinel /path/to/sentinel.conf

# 方式二:使用 redis-server 启动
redis-server /path/to/sentinel.conf --sentinel
Java 客户端连接
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.jedis.Jedis;
import java.util.HashSet;
import java.util.Set;

public class SentinelDemo {
    public static void main(String[] args) {
        // 哨兵节点集合
        Set<String> sentinels = new HashSet<>();
        sentinels.add("192.168.1.100:26379");
        sentinels.add("192.168.1.101:26379");
        sentinels.add("192.168.1.102:26379");
        
        // 创建哨兵连接池
        JedisSentinelPool pool = new JedisSentinelPool(
            "mymaster",  // 主节点名称
            sentinels,   // 哨兵节点
            "your_password"  // 密码
        );
        
        // 获取连接(自动路由到当前主节点)
        try (Jedis jedis = pool.getResource()) {
            jedis.set("key1", "value1");
            String value = jedis.get("key1");
            System.out.println(value);
        }
        
        pool.close();
    }
}

3.4 故障转移流程

// 故障转移流程示例说明
public class FailoverProcess {
    /*
     * 1. 主观下线(SDOWN)
     *    - 单个哨兵认为主节点下线(ping 超时)
     * 
     * 2. 客观下线(ODOWN)
     *    - 足够数量(quorum)的哨兵认为主节点下线
     * 
     * 3. 选举领导者哨兵
     *    - 使用 Raft 算法选举
     * 
     * 4. 领导者执行故障转移
     *    a. 从从节点中选出一个新的主节点
     *       - 排除不健康的从节点
     *       - 优先选择复制偏移量大的
     *       - 优先选择运行 ID 小的
     *    b. 让其他从节点复制新主节点
     *    c. 将旧主节点设置为从节点
     *    d. 通知客户端新主节点地址
     */
}

3.5 哨兵命令

// 哨兵管理命令
public class SentinelCommands {
    public static void main(String[] args) {
        Jedis sentinel = new Jedis("192.168.1.100", 26379);
        
        // 查看主节点信息
        Map<String, String> masterInfo = sentinel.sentinelMaster("mymaster");
        
        // 查看从节点列表
        List<Map<String, String>> slaves = 
            sentinel.sentinelSlaves("mymaster");
        
        // 查看哨兵列表
        List<Map<String, String>> sentinels = 
            sentinel.sentinelSentinels("mymaster");
        
        // 获取当前主节点地址
        List<String> master = sentinel.sentinelGetMasterAddrByName("mymaster");
        String masterHost = master.get(0);
        int masterPort = Integer.parseInt(master.get(1));
        
        // 重置主节点(用于测试)
        sentinel.sentinelReset("mymaster");
        
        // 强制故障转移
        sentinel.sentinelFailover("mymaster");
        
        sentinel.close();
    }
}

3.6 优缺点分析

优点

  • 自动故障转移
  • 配置相对简单
  • 客户端自动发现新主节点

缺点

  • 无法横向扩展写能力
  • 配置和管理相对复杂
  • 需要足够数量的哨兵节点

四、Redis Cluster

4.1 原理介绍

Redis Cluster 是 Redis 官方提供的分布式解决方案,支持数据分片和自动故障转移。

核心特点

  • 无中心架构,所有节点互联
  • 数据分片存储,支持横向扩展
  • 自动分片和重新分片
  • 自动故障转移

4.2 数据分片原理

哈希槽(Hash Slot)

Redis Cluster 将数据划分为 16384 个哈希槽,每个节点负责一部分槽。

Key → CRC16(key) % 16384 → Slot → Node
// 哈希槽计算示例
public class HashSlotDemo {
    public static void main(String[] args) {
        String key = "user:1001";
        
        // 计算 CRC16
        int crc16 = CRC16.crc16(key.getBytes());
        
        // 计算槽位
        int slot = crc16 % 16384;
        
        System.out.println("Key: " + key);
        System.out.println("Slot: " + slot);
        
        // Jedis 计算
        Jedis jedis = new Jedis("localhost", 6379);
        int jedisSlot = jedis.clusterKeySlot(key).intValue();
        System.out.println("Jedis Slot: " + jedisSlot);
        jedis.close();
    }
}

// CRC16 实现(简化版)
class CRC16 {
    private static final int[] CRC16_TABLE = new int[256];
    
    static {
        for (int i = 0; i < 256; i++) {
            int crc = i;
            for (int j = 0; j < 8; j++) {
                if ((crc & 1) == 1) {
                    crc = (crc >>> 1) ^ 0xA001;
                } else {
                    crc >>>= 1;
                }
            }
            CRC16_TABLE[i] = crc;
        }
    }
    
    public static int crc16(byte[] bytes) {
        int crc = 0;
        for (byte b : bytes) {
            crc = (crc >>> 8) ^ CRC16_TABLE[(crc ^ b) & 0xFF];
        }
        return crc & 0xFFFF;
    }
}
槽位分配示例
节点 A0 - 5460
节点 B5461 - 10922
节点 C:10923 - 16383

4.3 集群搭建

配置文件
# redis-7000.conf(每个节点配置文件)

# 端口
port 7000

# 开启集群模式
cluster-enabled yes

# 集群配置文件(自动生成)
cluster-config-file nodes-7000.conf

# 节点超时时间
cluster-node-timeout 15000

# 开启 AOF
appendonly yes

# 后台运行
daemonize yes

# 数据目录
dir /var/redis/7000
创建集群
# 创建集群(3主3从)
redis-cli --cluster create \
  192.168.1.100:7000 \
  192.168.1.100:7001 \
  192.168.1.100:7002 \
  192.168.1.101:7000 \
  192.168.1.101:7001 \
  192.168.1.101:7002 \
  --cluster-replicas 1

# 参数说明:
# --cluster-replicas 1 表示每个主节点有1个从节点
Java 客户端连接
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.HostAndPort;
import java.util.Set;
import java.util.HashSet;

public class ClusterDemo {
    public static void main(String[] args) {
        // 集群节点集合
        Set<HostAndPort> nodes = new HashSet<>();
        nodes.add(new HostAndPort("192.168.1.100", 7000));
        nodes.add(new HostAndPort("192.168.1.100", 7001));
        nodes.add(new HostAndPort("192.168.1.100", 7002));
        nodes.add(new HostAndPort("192.168.1.101", 7000));
        nodes.add(new HostAndPort("192.168.1.101", 7001));
        nodes.add(new HostAndPort("192.168.1.101", 7002));
        
        // 创建集群连接
        JedisCluster cluster = new JedisCluster(nodes);
        
        // 基本操作
        cluster.set("key1", "value1");
        String value = cluster.get("key1");
        System.out.println(value);
        
        // 批量操作(需要使用 Pipeline 或 Lua 脚本)
        // 注意:不同槽位的 key 不能在同一个事务中
        
        cluster.close();
    }
}

4.4 集群操作命令

// 集群管理命令
public class ClusterCommands {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.1.100", 7000);
        
        // 查看集群信息
        String clusterInfo = jedis.clusterInfo();
        System.out.println(clusterInfo);
        
        // 查看集群节点
        String nodes = jedis.clusterNodes();
        System.out.println(nodes);
        
        // 查看槽位分配
        // cluster slots 命令返回槽位范围和节点信息
        List<Object> slots = jedis.clusterSlots();
        
        // 查看指定 key 的槽位
        int slot = jedis.clusterKeySlot("user:1001").intValue();
        System.out.println("Slot: " + slot);
        
        // 查看槽位所在的节点
        String node = jedis.clusterNodeForKey(slot);
        System.out.println("Node: " + node);
        
        jedis.close();
    }
}

4.5 故障转移

// 集群故障转移示例
public class ClusterFailover {
    /*
     * 故障检测:
     * 1. 主观下线(PFAIL):单个节点认为某节点下线
     * 2. 客观下线(FAIL):多数主节点认为某主节点下线
     * 
     * 故障转移流程:
     * 1. 从节点取消复制状态
     * 2. 从节点竞选成为新主节点
     * 3. 其他从节点开始复制新主节点
     * 4. 集群更新槽位映射
     */
    
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.1.100", 7001);
        
        // 手动触发故障转移(在从节点上执行)
        jedis.clusterFailover();
        
        // 强制故障转移(不需要主节点同意)
        jedis.clusterFailoverForce();
        
        // 接管故障转移(用于特定场景)
        jedis.clusterFailoverTakeover();
        
        jedis.close();
    }
}

4.6 集群扩缩容

添加节点
# 添加主节点
redis-cli --cluster add-node \
  新节点IP:端口 \
  集群中任一节点IP:端口

# 添加从节点
redis-cli --cluster add-node \
  新节点IP:端口 \
  集群中任一节点IP:端口 \
  --cluster-slave \
  --cluster-master-id 主节点ID

# 重新分配槽位
redis-cli --cluster reshard \
  集群中任一节点IP:端口
// Java 客户端扩容后自动感知
public class ClusterResizeDemo {
    public static void main(String[] args) {
        Set<HostAndPort> nodes = new HashSet<>();
        nodes.add(new HostAndPort("192.168.1.100", 7000));
        // ... 添加新节点
        
        JedisCluster cluster = new JedisCluster(nodes);
        // 客户端会自动获取最新的槽位映射
        
        cluster.close();
    }
}
删除节点
# 删除节点(需要先迁移槽位)
redis-cli --cluster del-node \
  集群中任一节点IP:端口 \
  要删除的节点ID

4.7 优缺点分析

优点

  • 横向扩展能力强
  • 自动数据分片
  • 自动故障转移
  • 无中心架构

缺点

  • 配置和维护复杂
  • 批量操作受限
  • 事务支持有限(同一槽位)
  • 客户端实现复杂

五、实战应用场景

5.1 高可用缓存方案

// 完整的高可用缓存方案
public class HighAvailabilityCache {
    private JedisCluster cluster;
    
    public HighAvailabilityCache(Set<HostAndPort> nodes) {
        GenericObjectPoolConfig<Jedis> poolConfig = new GenericObjectPoolConfig<>();
        poolConfig.setMaxTotal(100);
        poolConfig.setMaxIdle(50);
        poolConfig.setMinIdle(10);
        poolConfig.setMaxWaitMillis(3000);
        poolConfig.setTestOnBorrow(true);
        poolConfig.setTestOnReturn(false);
        poolConfig.setTestWhileIdle(true);
        
        this.cluster = new JedisCluster(nodes, 2000, 2000, 5, 
                                         "password", poolConfig);
    }
    
    // 带重试的缓存读取
    public String getWithRetry(String key, int maxRetries) {
        for (int i = 0; i < maxRetries; i++) {
            try {
                return cluster.get(key);
            } catch (Exception e) {
                if (i == maxRetries - 1) {
                    throw new RuntimeException("Failed after " + maxRetries + " retries", e);
                }
                try {
                    Thread.sleep(100 * (i + 1));
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Interrupted", ie);
                }
            }
        }
        return null;
    }
    
    // 分布式锁(集群版)
    public boolean tryLock(String key, String value, int expireSeconds) {
        try {
            String result = cluster.set(key, value, "NX", "EX", expireSeconds);
            return "OK".equals(result);
        } catch (Exception e) {
            return false;
        }
    }
    
    public void unlock(String key, String value) {
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        cluster.eval(script, 1, key, value);
    }
    
    public void close() {
        try {
            cluster.close();
        } catch (Exception e) {
            // ignore
        }
    }
}

5.2 多Key操作优化

// 使用 Hash Tag 确保多个 key 在同一槽位
public class MultiKeyDemo {
    private JedisCluster cluster;
    
    public MultiKeyDemo(JedisCluster cluster) {
        this.cluster = cluster;
    }
    
    // 使用 Hash Tag
    // {user:1001}:profile 和 {user:1001}:settings 会在同一槽位
    public void saveUserWithHashTag(long userId) {
        String tag = "{user:" + userId + "}";
        
        // 同一用户的不同数据使用相同的 Hash Tag
        cluster.set(tag + ":profile", "profile data");
        cluster.set(tag + ":settings", "settings data");
        cluster.set(tag + ":history", "history data");
        
        // 可以使用事务
        Transaction tx = null;
        try {
            // 注意:JedisCluster 的事务支持有限
            // 实际生产中建议使用 Lua 脚本
            cluster.eval(
                "redis.call('set', KEYS[1], ARGV[1]); " +
                "redis.call('set', KEYS[2], ARGV[2]); " +
                "redis.call('set', KEYS[3], ARGV[3]);",
                3,
                tag + ":profile", tag + ":settings", tag + ":history",
                "profile data", "settings data", "history data"
            );
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    // 批量获取(Pipeline)
    public List<String> multiGet(List<String> keys) {
        // 对于集群,需要按节点分组
        Map<JedisPool, List<String>> nodeKeys = groupKeysByNode(keys);
        
        List<String> results = new ArrayList<>();
        for (Map.Entry<JedisPool, List<String>> entry : nodeKeys.entrySet()) {
            try (Jedis jedis = entry.getKey().getResource()) {
                Pipeline pipeline = jedis.pipelined();
                for (String key : entry.getValue()) {
                    pipeline.get(key);
                }
                List<Object> objects = pipeline.syncAndReturnAll();
                for (Object obj : objects) {
                    results.add((String) obj);
                }
            }
        }
        return results;
    }
    
    private Map<JedisPool, List<String>> groupKeysByNode(List<String> keys) {
        // 实现按节点分组的逻辑
        // 这里简化处理,实际需要根据槽位映射
        return new HashMap<>();
    }
}

六、集群方案对比

特性主从复制哨兵模式Redis Cluster
数据分片
自动故障转移
横向扩展写能力
配置复杂度
客户端复杂度
运维复杂度
适用场景数据备份、读写分离高可用、自动故障转移大规模数据、高并发

七、总结与最佳实践

7.1 方案选择建议

场景推荐方案
数据量小,允许短暂不可用主从复制
数据量小,需要高可用哨兵模式
数据量大,需要横向扩展Redis Cluster
数据量大 + 高可用Redis Cluster

7.2 最佳实践

  1. 哨兵模式

    • 至少部署 3 个哨兵节点
    • 哨兵节点可以和 Redis 节点混部
    • 配置合理的超时时间
  2. Redis Cluster

    • 至少 3 个主节点
    • 每个主节点至少 1 个从节点
    • 合理规划槽位分配
    • 使用 Hash Tag 处理多 Key 操作
  3. 客户端配置

    • 配置合理的连接池参数
    • 实现重试机制
    • 处理重定向(MOVED、ASK)
  4. 监控告警

    • 监控节点状态
    • 监控内存使用率
    • 监控集群健康度
    • 设置合理的告警阈值

7.3 常见问题

  1. 脑裂问题:配置 min-replicas-to-writemin-replicas-max-lag
  2. 数据丢失:合理配置持久化和复制参数
  3. 性能问题:避免大 Key、热点 Key,合理使用 Pipeline

八、思考与练习

思考题

  1. 基础题:主从复制、哨兵模式、Redis Cluster 三种方案各自的核心特点是什么?它们分别解决了什么问题?

  2. 进阶题:Redis Cluster 使用 16384 个哈希槽进行数据分片,请分析为什么选择这个数字?哈希槽相比一致性哈希有什么优势?

  3. 实战题:设计一个方案,当 Redis Cluster 某个主节点宕机时,如何保证数据不丢失且服务快速恢复?

编程练习

练习:基于 Redis Cluster 实现一个分布式会话管理器,要求:

  • 支持会话创建、获取、删除、过期
  • 使用 Hash Tag 确保同一用户的所有数据在同一槽位
  • 实现会话的自动续期功能

提示:使用 {session:sessionId} 作为 Hash Tag 前缀。

章节关联

  • 前置章节:《Redis基础详解》- 掌握 Redis 数据结构和持久化
  • 后续章节:《缓存设计详解》- 学习缓存架构设计最佳实践
  • 扩展阅读:《Redis 设计与实现》第 16-18 章

📝 下一章预告

下一章将探讨缓存系统设计的核心问题,包括缓存穿透、缓存击穿、缓存雪崩的防护策略,以及缓存预热、缓存更新等最佳实践。


本章完