Redis哨兵模式深度解析引言Redis哨兵模式Sentinel是Redis官方提供的高可用解决方案用于监控Redis主从架构的运行状态并在主节点发生故障时自动进行故障转移实现Redis服务的高可用性。哨兵模式能够监控多个Redis实例自动完成主从切换并通知应用程序新的主节点信息。哨兵架构1.1 架构概述┌──────────────────────────────────────────────────────────┐ │ 应用程序 │ │ ┌─────────────────────────────┐ │ │ │ Redis Sentinel Client │ │ │ └──────────────┬──────────────┘ │ └─────────────────────────┼────────────────────────────────┘ │ ┌─────────────────┼─────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Sentinel 1 │ │ Sentinel 2 │ │ Sentinel 3 │ │ (Monitor) │ │ (Monitor) │ │ (Monitor) │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ └──────────────────┼──────────────────┘ │ ┌─────────────────────────┼────────────────────────────────┐ │ ┌───────────┴───────────┐ │ │ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ Master │───────│ Slave │ │ │ │ (192.168.1) │ │ (192.168.2) │ │ │ └──────────────┘ └──────────────┘ │ └───────────────────────────────────────────────────────────┘1.2 哨兵职责哨兵模式主要完成以下任务监控Monitoring持续检查主从服务器是否正常运行通知Notification当被监控的Redis发生问题时通过API通知管理员自动故障转移Automatic Failover主节点故障时自动将从节点升级为主节点配置提供者Configuration Provider客户端通过哨兵获取当前的主节点地址哨兵配置2.1 哨兵配置文件# sentinel.conf # 哨兵端口 port 26379 # 绑定地址 bind 0.0.0.0 # 日志文件 logfile /var/log/redis/sentinel.log # 工作目录 dir /tmp # 监控配置 # sentinel monitor master-name ip port quorum sentinel monitor mymaster 192.168.1.100 6379 2 # 主节点认证密码 sentinel auth-pass mymaster password123 # 故障转移超时时间 sentinel down-after-milliseconds mymaster 30000 # 并行同步从节点数量 sentinel parallel-syncs mymaster 1 # 故障转移超时时间 sentinel failover-timeout mymaster 180000 # 自动发现其他哨兵 sentinel discover-clusters 1 # 客户端重新配置主节点通知 sentinel notification-script mymaster /var/redis/notify.sh # 故障转移后执行的脚本 sentinel client-reconfig-script mymaster /var/redis/reconfig.sh2.2 主从节点配置# 主节点配置 - redis-master.conf port 6379 bind 0.0.0.0 daemonize yes pidfile /var/run/redis_6379.pid logfile /var/log/redis/redis-master.log dir /data/redis requirepass master_password masterauth master_password maxmemory 2gb maxmemory-policy allkeys-lru# 从节点配置 - redis-slave.conf port 6380 bind 0.0.0.0 daemonize yes pidfile /var/run/redis_6380.pid logfile /var/log/redis/redis-slave.log dir /data/redis requirepass slave_password masterauth master_password slaveof 192.168.1.100 6379 replica-read-only yes repl-diskless-sync yes2.3 启动哨兵# 启动哨兵 redis-sentinel /etc/redis/sentinel.conf # 或者 redis-server /etc/redis/sentinel.conf --sentinel # 检查哨兵状态 redis-cli -p 26379 INFO sentinel # 查看监控的主节点 redis-cli -p 26379 SENTINEL masters # 查看指定主节点详情 redis-cli -p 26379 SENTINEL master mymaster # 查看从节点列表 redis-cli -p 26379 SENTINEL slaves mymasterJava客户端集成3.1 Jedis哨兵客户端import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import redis.clients.jedis.JedisSentinelPool; import java.util.HashSet; import java.util.Set; Configuration public class RedisSentinelConfiguration { Value(${redis.sentinel.master-name}) private String masterName; Value(${redis.sentinel.nodes}) private String sentinelNodes; Value(${redis.sentinel.password:}) private String sentinelPassword; Value(${redis.master.password}) private String masterPassword; Bean public JedisSentinelPool jedisSentinelPool() { SetString sentinels new HashSet(); for (String node : sentinelNodes.split(,)) { sentinels.add(node.trim()); } GenericObjectPoolConfig? poolConfig new GenericObjectPoolConfig(); poolConfig.setMaxTotal(100); poolConfig.setMaxIdle(50); poolConfig.setMinIdle(10); poolConfig.setMaxWaitMillis(3000); poolConfig.setTestOnBorrow(true); poolConfig.setTestOnReturn(true); poolConfig.setTestWhileIdle(true); return new JedisSentinelPool( masterName, sentinels, poolConfig, 3000, masterPassword ); } Bean public JedisConnectionFactory jedisConnectionFactory( JedisSentinelPool jedisSentinelPool) { return new JedisConnectionFactory( new RedisSentinelConfiguration(masterName, parseSentinelNodes(sentinelNodes))); } private SetRedisNode parseSentinelNodes(String nodes) { SetRedisNode sentinelNodesSet new HashSet(); for (String node : nodes.split(,)) { String[] parts node.trim().split(:); sentinelNodesSet.add(new RedisNode( parts[0], Integer.parseInt(parts[1]))); } return sentinelNodesSet; } }3.2 Spring Boot集成# application.yml spring: redis: sentinel: master: mymaster nodes: 192.168.1.101:26379,192.168.1.102:26379,192.168.1.103:26379 password: sentinel_password password: master_password lettuce: pool: max-active: 100 max-idle: 50 min-idle: 10 max-wait: 3000msimport org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.util.HashSet; import java.util.Set; Configuration EnableConfigurationProperties(RedisProperties.class) public class RedisSentinelConfig { Bean public RedisSentinelConfiguration redisSentinelConfiguration( RedisProperties properties) { RedisSentinelConfiguration config new RedisSentinelConfiguration(); config.setMaster(properties.getSentinel().getMaster()); SetRedisNode nodes new HashSet(); for (String node : properties.getSentinel().getNodes()) { String[] parts node.split(:); nodes.add(new RedisNode(parts[0], Integer.parseInt(parts[1]))); } config.setSentinels(nodes); if (properties.getPassword() ! null) { config.setPassword(properties.getPassword()); } return config; } Bean public RedisConnectionFactory redisConnectionFactory( RedisSentinelConfiguration sentinelConfig) { return new JedisConnectionFactory(sentinelConfig); } Bean public RedisTemplateString, Object redisTemplate( RedisConnectionFactory connectionFactory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer( new GenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer( new GenericJackson2JsonRedisSerializer()); return template; } }3.3 动态故障转移处理import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisSentinelConnection; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.stereotype.Component; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisSentinelPool; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.HashSet; import java.util.Set; Component public class SentinelFailoverHandler { Autowired private JedisSentinelPool sentinelPool; private Jedis jedis; PostConstruct public void init() { jedis sentinelPool.getResource(); } PreDestroy public void destroy() { if (jedis ! null) { jedis.close(); } } /** * 获取当前主节点信息 */ public RedisMasterInfo getCurrentMaster() { Jedis jedis sentinelPool.getResource(); try { String host jedis Sentinel(master, sentinelPool.getCurrentHostMaster()); int port sentinelPool.getCurrentHostMaster().getPort(); return new RedisMasterInfo(host, port); } finally { jedis.close(); } } /** * 监听主节点变更事件 */ public void subscribeToMasterChanges() { SetString sentinelSet new HashSet(); for (Jedis sentinel : sentinelPool.getSentinels()) { sentinelSet.add(sentinel.getClient().getHost() : sentinel.getClient().getPort()); } // 订阅 switch-master 频道 for (String sentinelAddr : sentinelSet) { String[] parts sentinelAddr.split(:); try (Jedis jedis new Jedis(parts[0], Integer.parseInt(parts[1]))) { jedis.subscribe(new JedisPubSub() { Override public void onMessage(String channel, String message) { handleMasterSwitch(message); } }, switch-master); } } } private void handleMasterSwitch(String message) { // 解析消息并更新配置 // message格式: mymaster 192.168.1.100 6379 192.168.1.101 6380 System.out.println(Master switched: message); // 更新应用程序配置 // 重新初始化连接池 } /** * 手动触发故障转移 */ public void triggerFailover(String masterName) { SetString sentinelSet new HashSet(); for (Jedis sentinel : sentinelPool.getSentinels()) { sentinelSet.add(sentinel.getClient().getHost() : sentinel.getClient().getPort()); } for (String sentinelAddr : sentinelSet) { String[] parts sentinelAddr.split(:); try (Jedis jedis new Jedis(parts[0], Integer.parseInt(parts[1]))) { jedis.sentinel(failover, masterName); break; } } } public static class RedisMasterInfo { private String host; private int port; public RedisMasterInfo(String host, int port) { this.host host; this.port port; } public String getHost() { return host; } public int getPort() { return port; } } }哨兵管理命令4.1 常用管理命令# 查看主节点信息 redis-cli -p 26379 SENTINEL master mymaster # 查看所有主节点 redis-cli -p 26379 SENTINEL masters # 查看指定主节点的从节点 redis-cli -p 26379 SENTINEL slaves mymaster # 查看其他哨兵节点 redis-cli -p 26379 SENTINEL sentinels mymaster # 获取主节点地址 redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster # 检查主节点可达性 redis-cli -p 26379 SENTINEL is-master-down-by-addr # 重置主节点状态 redis-cli -p 26379 SENTINEL reset mymaster # 强制故障转移 redis-cli -p 26379 SENTINEL failover mymaster # 检查哨兵状态 redis-cli -p 26379 INFO sentinel4.2 Java管理工具import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; Component public class SentinelAdminTool { Autowired private RedisTemplateString, String redisTemplate; /** * 获取所有主节点 */ public ListString getAllMasters() { return redisTemplate.execute((RedisConnection connection) - { RedisSentinelConnection sentinelConnection connection.getSentinelConnection(); return null; }); } /** * 获取主节点信息 */ public MapString, String getMasterInfo(String masterName) { // 使用Jedis客户端 return null; } /** * 移除不正常的从节点 */ public void removeSlave(String masterName, String slaveAddr) { // sentinel remove mymaster 192.168.1.102 6380 } /** * 动态添加从节点 */ public void addSlave(String masterName, String slaveAddr, int port) { // sentinel set mymaster slave-addr slave-host:port } /** * 修改主节点密码 */ public void setMasterPassword(String masterName, String password) { // sentinel set mymaster auth-pass password } }故障转移机制5.1 故障检测public class FailoverMechanism { /** * 故障检测流程 */ public void detectFailure() { // 1. 主观下线检测 // 哨兵每秒向所有节点发送PING命令 // 如果超过down-after-milliseconds未收到响应标记为主观下线 // 2. 客观下线检测 // 询问其他哨兵是否也认为主节点下线 // 如果超过quorum数量的哨兵认为下线标记为客观下线 // 3. 故障转移 // 从从节点中选择新的主节点 // 通知所有从节点同步新主节点 // 更新客户端配置 } /** * 选举新主节点策略 */ public String electNewMaster(ListSlaveInfo slaves) { // 1. 过滤不正常的从节点 // 2. 按照优先级排序replica-priority // 3. 选择复制偏移量最大的从节点 // 4. 选择运行ID最小的从节点 return slaves.stream() .filter(s - s.getRole().equals(slave)) .filter(s - s.getLinkStatus().equals(connected)) .sorted((s1, s2) - { // 比较优先级 int priorityCompare Integer.compare(s2.getReplicationPriority(), s1.getReplicationPriority()); if (priorityCompare ! 0) { return priorityCompare; } // 比较复制偏移量 return Long.compare( s2.getMasterLinkOffset(), s1.getMasterLinkOffset()); }) .findFirst() .map(SlaveInfo::getIp) .orElse(null); } public static class SlaveInfo { private String ip; private int port; private String role; private String linkStatus; private int replicationPriority; private long masterLinkOffset; public String getIp() { return ip; } public String getRole() { return role; } public String getLinkStatus() { return linkStatus; } public int getReplicationPriority() { return replicationPriority; } public long getMasterLinkOffset() { return masterLinkOffset; } } }5.2 故障恢复通知#!/bin/bash # notify.sh - 故障通知脚本 MASTER_NAME$1 STATE$2 MASTER_IP$3 MASTER_PORT$4 # 新主节点信息故障转移时 NEW_MASTER_IP$5 NEW_MASTER_PORT$6 case $STATE in odown) echo Master $MASTER_NAME is now down (objective down) ;; -odown) echo Master $MASTER_NAME is back online ;; try-failover) echo Attempting to failover master $MASTER_NAME ;; failover-state-selecting) echo Failover state: selecting new master ;; failover-state-send-slaveof-noone) echo Sending SLAVEOF NO ONE to selected slave ;; failover-state-promoted) echo Promoted slave to new master ;; failover-state-reconf-slaves) echo Reconfiguring slaves to new master ;; failover-end) echo Failover completed successfully # 发送通知 curl -X POST http://alert-service/notify \ -d {\master\:\$MASTER_NAME\,\new_master\:\$NEW_MASTER_IP:$NEW_MASTER_PORT\} ;; -failover-abort) echo Failover aborted ;; sentinel-address-switch) echo Sentinel address switched ;; slave-reconf-sent) echo Slave reconfiguration sent ;; slave-reconf-inprog) echo Slave reconfiguration in progress ;; slave-reconf-done) echo Slave reconfiguration completed ;; esac监控与告警6.1 哨兵监控import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; Component public class SentinelMonitor { Autowired private SentinelFailoverHandler failoverHandler; Scheduled(fixedDelay 10000) // 每10秒检查一次 public void monitorSentinelHealth() { MapString, Object status new HashMap(); try { // 检查主节点 RedisMasterInfo master failoverHandler.getCurrentMaster(); status.put(currentMaster, master); status.put(status, HEALTHY); } catch (Exception e) { status.put(status, UNHEALTHY); status.put(error, e.getMessage()); sendAlert(Sentinel health check failed: e.getMessage()); } System.out.println(Sentinel status: status); } Scheduled(fixedDelay 60000) // 每分钟检查一次 public void checkReplicationLag() { // 检查复制延迟 } private void sendAlert(String message) { // 发送告警 System.err.println(ALERT: message); } }6.2 配置检查public class SentinelConfigValidator { public static boolean validateConfig( MapString, Object config) { // 1. 检查quorum配置 int quorum (Integer) config.get(quorum); int sentinelCount (Integer) config.get(sentinelCount); if (quorum sentinelCount) { System.err.println(Error: quorum must be less than sentinel count); return false; } // 2. 检查down-after-milliseconds配置 int downAfterMs (Integer) config.get(downAfterMilliseconds); if (downAfterMs 10000) { System.err.println(Warning: down-after-milliseconds is too low); } // 3. 检查parallel-syncs配置 int parallelSyncs (Integer) config.get(parallelSyncs); int slaveCount (Integer) config.get(slaveCount); if (parallelSyncs slaveCount) { System.err.println(Warning: parallel-syncs exceeds slave count); } return true; } }最佳实践7.1 部署建议# 部署最佳实践 # 1. 至少部署3个哨兵节点奇数个 # 2. 哨兵节点应该部署在不同机器上 # 3. 哨兵节点应该与Redis节点网络互通 # 4. 配置合理的quorum值建议: 哨兵数量/2 17.2 配置模板# 生产环境哨兵配置模板 # 基础配置 port 26379 daemonize yes pidfile /var/run/redis-sentinel.pid logfile /var/log/redis/sentinel.log dir /tmp # 监控配置 # 监控主节点可配置多个 sentinel monitor mymaster 192.168.1.100 6379 2 sentinel auth-pass mymaster master_password # 故障检测超时 sentinel down-after-milliseconds mymaster 30000 sentinel failover-timeout mymaster 180000 # 并行同步配置 sentinel parallel-syncs mymaster 1 # 客户端配置 sentinel client-reconnect-timeout mymaster 30000 # 通知配置 sentinel notification-script mymaster /var/redis/notify.sh sentinel client-reconfig-script mymaster /var/redis/reconfig.sh # 性能配置 sentinel announce-ip 192.168.1.101 sentinel announce-port 26379总结Redis哨兵模式是实现Redis高可用性的重要方案通过监控主从节点的运行状态和自动故障转移确保Redis服务的持续可用。在实际部署中需要合理配置哨兵节点数量、quorum值、故障检测超时等参数并建立完善的监控告警机制确保故障发生时能够及时处理。