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面试圣经