8.3 Redis高可用方案

面试重要程度:⭐⭐⭐⭐⭐ 常见提问方式: 主从复制原理、哨兵选举机制、集群分片策略 预计阅读时间:50分钟

🎯 主从复制、哨兵模式、集群模式

主从复制原理

面试官: "Redis主从复制是如何工作的?全量同步和增量同步的区别是什么?"

必答要点:

复制流程详解:

/** * Redis主从复制三个阶段 */public class RedisReplication {        /**     * 第一阶段:建立连接     */    public void connectionPhase() {        /**         * 1. 从库执行 SLAVEOF master_ip master_port         * 2. 从库保存主库信息         * 3. 从库创建与主库的socket连接         * 4. 从库发送PING命令测试连接         * 5. 身份验证(如果设置了密码)         * 6. 从库发送端口信息给主库         */    }        /**     * 第二阶段:数据同步     */    public void dataSyncPhase() {        /**         * 全量同步(SYNC):         * 1. 从库发送PSYNC命令         * 2. 主库执行BGSAVE生成RDB文件         * 3. 主库将RDB文件发送给从库         * 4. 从库清空数据库,载入RDB文件         * 5. 主库将缓冲区的写命令发送给从库         */    }        /**     * 第三阶段:命令传播     */    public void commandPropagationPhase() {        /**         * 增量同步:         * 1. 主库执行写命令后,将命令发送给从库         * 2. 从库执行相同的写命令         * 3. 保持主从数据一致性         */    }}

PSYNC优化机制:

/** * Redis 2.8+ PSYNC命令优化 */public class PSyncOptimization {        /**     * 部分重同步机制     */    public void partialResync() {        /**         * 复制偏移量(replication offset):         * - 主库和从库都维护一个偏移量         * - 主库每传播N个字节,偏移量+N         * - 从库每接收N个字节,偏移量+N         *          * 复制积压缓冲区(replication backlog):         * - 主库维护的固定长度FIFO队列         * - 默认大小1MB         * - 保存最近传播的写命令         *          * 服务器运行ID(run ID):         * - 每个Redis服务器的唯一标识         * - 服务器启动时生成40位随机字符串         */                /**         * 部分重同步条件:         * 1. 从库记录的主库run ID与当前主库run ID相同         * 2. 从库的复制偏移量在复制积压缓冲区范围内         *          * 满足条件:执行部分重同步         * 不满足条件:执行全量同步         */    }}

哨兵模式(Sentinel)

面试官: "Redis哨兵是如何实现自动故障转移的?选举过程是怎样的?"

哨兵架构:

# sentinel.conf 配置示例port 26379sentinel monitor mymaster 127.0.0.1 6379 2sentinel down-after-milliseconds mymaster 5000sentinel parallel-syncs mymaster 1sentinel failover-timeout mymaster 10000​# 哨兵集群配置(至少3个节点)# sentinel1: 26379# sentinel2: 26380  # sentinel3: 26381

故障检测机制:

/** * 哨兵故障检测 */public class SentinelFailureDetection {        /**     * 主观下线(Subjectively Down)     */    public void subjectiveDown() {        /**         * 检测流程:         * 1. 哨兵每秒向主库发送PING命令         * 2. 如果超过down-after-milliseconds没有响应         * 3. 哨兵标记主库为主观下线(SDOWN)         *          * 注意:只是单个哨兵的判断,可能是网络问题         */    }        /**     * 客观下线(Objectively Down)     */    public void objectiveDown() {        /**         * 检测流程:         * 1. 哨兵发现主库主观下线后         * 2. 向其他哨兵发送SENTINEL is-master-down-by-addr命令         * 3. 如果超过quorum个哨兵认为主库下线         * 4. 标记主库为客观下线(ODOWN)         * 5. 开始故障转移流程         */    }}

选举和故障转移:

/** * 哨兵选举和故障转移 */public class SentinelElection {        /**     * 选举领导者哨兵     */    public void leaderElection() {        /**         * Raft算法选举:         * 1. 每个哨兵都可以成为候选者         * 2. 候选者向其他哨兵发送投票请求         * 3. 每个哨兵在一轮选举中只能投票给一个候选者         * 4. 获得超过半数选票的候选者成为领导者         * 5. 如果没有候选者获得半数选票,重新选举         */    }        /**     * 故障转移流程     */    public void failoverProcess() {        /**         * 1. 从从库中选择新的主库:         *    - 排除下线的从库         *    - 排除5秒内没有回复INFO命令的从库         *    - 排除与原主库断开连接超过10*down-after-milliseconds的从库         *    - 选择优先级最高的从库(replica-priority)         *    - 选择复制偏移量最大的从库(数据最新)         *    - 选择运行ID最小的从库         *          * 2. 将选中的从库升级为主库:         *    - 向从库发送SLAVEOF NO ONE命令         *          * 3. 修改其他从库的主库地址:         *    - 向其他从库发送SLAVEOF new_master_ip new_master_port         *          * 4. 更新客户端配置:         *    - 通过发布订阅机制通知客户端主库变更         */    }}

哨兵客户端实现:

/** * Spring Boot整合哨兵模式 */@Configurationpublic class RedisSentinelConfig {        @Bean    public LettuceConnectionFactory redisConnectionFactory() {        // 哨兵配置        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()            .master("mymaster")            .sentinel("127.0.0.1", 26379)            .sentinel("127.0.0.1", 26380)            .sentinel("127.0.0.1", 26381);                // 连接池配置        GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();        poolConfig.setMaxTotal(20);        poolConfig.setMaxIdle(10);        poolConfig.setMinIdle(5);                LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()            .poolConfig(poolConfig)            .build();                return new LettuceConnectionFactory(sentinelConfig, clientConfig);    }        @Bean    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {        RedisTemplate<String, Object> template = new RedisTemplate<>();        template.setConnectionFactory(connectionFactory);                // 序列化配置        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);        template.setDefaultSerializer(serializer);        template.setKeySerializer(new StringRedisSerializer());                return template;    }}​/** * 哨兵事件监听 */@Component@Slf4jpublic class SentinelEventListener {        @EventListener    public void handleMasterSwitched(RedisMasterReplicaChangedEvent event) {        log.info("主库切换事件: 旧主库={}:{}, 新主库={}:{}",             event.getOldMaster().getHost(), event.getOldMaster().getPort(),            event.getNewMaster().getHost(), event.getNewMaster().getPort());                // 可以在这里执行额外的业务逻辑        // 比如清理缓存、通知监控系统等    }}

集群模式(Cluster)

面试官: "Redis集群是如何实现数据分片的?槽位分配算法是什么?"

集群架构原理:

/** * Redis集群核心概念 */public class RedisCluster {        /**     * 哈希槽(Hash Slot)     */    public void hashSlot() {        /**         * 槽位设计:         * - Redis集群共有16384个槽位(0-16383)         * - 每个主节点负责一部分槽位         * - 数据根据key的CRC16校验值分配到对应槽位         *          * 分片算法:         * slot = CRC16(key) % 16384         *          * 优势:         * - 支持动态扩容缩容         * - 槽位可以在节点间迁移         * - 客户端可以直接计算key所在节点         */    }        /**     * 节点通信(Gossip协议)     */    public void gossipProtocol() {        /**         * 集群节点通信:         * 1. 每个节点维护集群状态信息         * 2. 定期与其他节点交换信息(Gossip消息)         * 3. 包含节点状态、槽位分配、故障信息等         * 4. 最终达到集群状态一致性         *          * 消息类型:         * - MEET: 加入集群         * - PING: 心跳检测         * - PONG: 心跳响应         * - FAIL: 节点故障         * - PUBLISH: 发布消息         */    }}

集群搭建配置:

# 集群节点配置(redis.conf)port 7000cluster-enabled yescluster-config-file nodes-7000.confcluster-node-timeout 5000appendonly yes​# 启动6个节点(3主3从)redis-server redis-7000.confredis-server redis-7001.confredis-server redis-7002.confredis-server redis-7003.confredis-server redis-7004.confredis-server redis-7005.conf​# 创建集群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

客户端实现:

/**
 * Spring Boot整合Redis集群
 */
@Configuration
public class RedisClusterConfig {
    
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        // 集群节点配置
        RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
        clusterConfig.clusterNode("127.0.0.1", 7000);
        clusterConfig.clusterNode("127.0.0.1", 7001);
        clusterConfig.clusterNode("127.0.0.1", 7002);
        clusterConfig.clusterNode("127.0.0.1", 7003);
        clusterConfig.clusterNode("127.0.0.1", 7004);
        clusterConfig.clusterNode("127.0.0.1", 7005);
        
        // 集群配置
        clusterConfig.setMaxRedirects(3);  // 最大重定向次数
        
        // 连接池配置
        GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
        poolConfig.setMaxTotal(50);
        poolConfig.setMaxIdle(20);
        poolConfig.setMinIdle(10);
        
        LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
            .poolConfig(poolConfig)
            .commandTimeout(Duration.ofSeconds(5))
            .build();
        
        return new LettuceConnectionFactory(clusterConfig, clientConfig);
    }
}

/**
 * 集群操作示例
 */
@Service
public class RedisClusterService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 批量操作优化
     */
    public void batchOperations() {
        // ❌ 普通批量操作可能涉及多个节点,效率低
        List<String> keys = Arrays.asList("user:1", "user:2", "user:3");
        List<Object> values = redisTemplate.opsForValue().multiGet(keys);
        
        // ✅ 使用Hash Tag确保相关数据在同一节点
        Map<String, String> userData = new HashMap<>();
        userData.put("{user:1001}:profile", "profile_data");
        userData.put("{user:1001}:settings", "settings_data");
        userData.put("{user:1001}:preferences", "preferences_data");
        
        // 这些key都会分配到同一个槽位,可以使用事务
        redisTemplate.opsForValue().multiSet(userData);
    }
    
    /**
     * 集群信息监控
     */
    public void clusterInfo() {
        RedisClusterConnection connection = redisTemplate.getConnectionFactory()
            .getClusterConnection();
        
        // 获取集群节点信息
        Iterable<RedisClusterNode> nodes = connection.clusterGetNodes();
        for (RedisClusterNode node : nodes) {
            System.out.printf("节点: %s:%d, 主节点: %s, 槽位: %s%n",
                node.getHost(), node.getPort(), 
                node.isMaster(), node.getSlotRange());
        }
        
        // 获取集群状态
        Properties clusterInfo = connection.clusterGetClusterInfo();
        System.out.println("集群状态: " + clusterInfo.getProperty("cluster_state"));
        System.out.println("已分配槽位: " + clusterInfo.getProperty("cluster_slots_assigned"));
    }
}

🔄 持久化机制(RDB vs AOF)

RDB持久化

面试官: "RDB和AOF持久化的区别是什么?各自的优缺点?"

RDB机制详解:

/**
 * RDB持久化机制
 */
public class RDBPersistence {
    
    /**
     * RDB触发方式
     */
    public void rdbTrigger() {
        /**
         * 1. 手动触发:
         * - SAVE命令:阻塞主进程
         * - BGSAVE命令:fork子进程执行
         * 
         * 2. 自动触发:
         * - 配置save规则:save 900 1(900秒内至少1次修改)
         * - 主从复制:主库自动执行BGSAVE
         * - SHUTDOWN命令:自动执行SAVE
         */
    }
    
    /**
     * RDB文件格式
     */
    public void rdbFormat() {
        /**
         * RDB文件结构:
         * [REDIS][版本][数据库][键值对][EOF][校验和]
         * 
         * 优化特性:
         * - 压缩存储:字符串、列表等数据压缩
         * - 类型编码:根据数据类型选择最优编码
         * - 过期时间:保存key的过期信息
         */
    }
}

RDB配置优化:

# redis.conf RDB配置
save 900 1      # 900秒内至少1个key被修改
save 300 10     # 300秒内至少10个key被修改  
save 60 10000   # 60秒内至少10000个key被修改

# RDB文件配置
dbfilename dump.rdb
dir /var/lib/redis
rdbcompression yes      # 启用压缩
rdbchecksum yes        # 启用校验和
stop-writes-on-bgsave-error yes  # BGSAVE失败时停止写入

AOF持久化

AOF机制详解:

/**
 * AOF持久化机制
 */
public class AOFPersistence {
    
    /**
     * AOF写入策略
     */
    public void aofSyncPolicy() {
        /**
         * 三种同步策略:
         * 
         * 1. always:每个写命令都同步到磁盘
         *    - 优点:数据安全性最高
         *    - 缺点:性能最差
         * 
         * 2. everysec:每秒同步一次(默认)
         *    - 优点:性能和安全性平衡
         *    - 缺点:最多丢失1秒数据
         * 
         * 3. no:由操作系统决定何时同步
         *    - 优点:性能最好
         *    - 缺点:数据安全性最差
         */
    }
    
    /**
     * AOF重写机制
     */
    public void aofRewrite() {
        /**
         * 重写触发条件:
         * - AOF文件大小超过上次重写后的100%
         * - AOF文件大小超过64MB
         * 
         * 重写过程:
         * 1. fork子进程执行重写
         * 2. 子进程遍历数据库,生成新的AOF文件
         * 3. 主进程继续处理命令,写入AOF缓冲区和重写缓冲区
         * 4. 子进程完成后,主进程将重写缓冲区内容追加到新AOF文件
         * 5. 原子性替换旧AOF文件
         */
    }
}

AOF配置优化:

# redis.conf AOF配置
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec

# AOF重写配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-rewrite-incremental-fsync yes

# 混合持久化(Redis 4.0+)
aof-use-rdb-preamble yes

持久化策略选择

/**
 * 持久化策略对比
 */
@Component
public class PersistenceStrategy {
    
    /**
     * 性能对比分析
     */
    public void performanceComparison() {
        /**
         * RDB优势:
         * - 文件紧凑,适合备份和灾难恢复
         * - 恢复速度快(直接加载到内存)
         * - 对Redis性能影响小(子进程执行)
         * 
         * RDB劣势:
         * - 数据安全性差(可能丢失最后一次快照后的数据)
         * - fork子进程时可能阻塞(数据量大时)
         * 
         * AOF优势:
         * - 数据安全性高(可配置同步策略)
         * - 文件可读,便于分析和修复
         * - 支持增量备份
         * 
         * AOF劣势:
         * - 文件体积大
         * - 恢复速度慢(需要重放命令)
         * - 对性能影响较大
         */
    }
    
    /**
     * 混合持久化(推荐)
     */
    public void hybridPersistence() {
        /**
         * Redis 4.0+ 混合持久化:
         * - AOF文件前半部分是RDB格式(全量数据)
         * - AOF文件后半部分是AOF格式(增量数据)
         * 
         * 优势:
         * - 结合了RDB和AOF的优点
         * - 恢复速度快,数据安全性高
         * - 文件体积相对较小
         */
    }
}

💡 面试回答要点

标准回答模板

主从复制问题:

"Redis主从复制分为三个阶段:连接建立、数据同步、命令传播。
数据同步包括全量同步和增量同步,Redis 2.8+引入PSYNC命令,
通过复制偏移量和复制积压缓冲区实现部分重同步,
避免了网络闪断导致的全量同步,提高了效率。"

哨兵模式问题:

"哨兵模式通过多个哨兵节点监控主库状态,实现自动故障转移。
包括主观下线和客观下线两个阶段,使用Raft算法选举领导者哨兵,
然后执行故障转移:选择最优从库升级为主库,
修改其他从库配置,通知客户端主库变更。"

集群模式问题:

"Redis集群通过一致性哈希的变种实现数据分片,
使用16384个哈希槽,支持动态扩容。
节点间通过Gossip协议通信,维护集群状态一致性。
客户端可以直接计算key所在节点,减少网络开销。"

持久化问题:

"RDB适合备份和快速恢复,但可能丢失数据;
AOF数据安全性高但恢复慢;
推荐使用混合持久化,结合两者优点。
生产环境通常配置主库AOF+从库RDB的组合策略。"

本节核心要点:

  • ✅ 主从复制的PSYNC优化机制
  • ✅ 哨兵模式的故障检测和选举算法
  • ✅ 集群模式的分片和通信机制
  • ✅ RDB和AOF持久化的原理和选择策略

总结: Redis高可用方案需要根据业务场景选择合适的架构,理解各种方案的原理和权衡,才能在生产环境中做出正确的技术决策

Java面试圣经 文章被收录于专栏

Java面试圣经

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务