主从技术的切换方法是:当主节点宕机后,需要人工将从节点切换为主节点,这样的方式费事费力还会造成一段时间内服务不可用,这样的方式显然不是我们需要的,所以我们必须要有一个高可用的方案来抵抗节点的故障,当节点发生故障时可以自动进行主从切换,程序可以不用重启,仿佛什么事情都没有发生一样。Redis官方提供了这样一种方案-——Redis Sentinel(Sentinel的含义是哨兵)。
一、Redis Sentinel 架构

如上图所示,我们可以将Redis Sentinel 集群看成是一个zookeeper集群,它是集群高可用的心脏,一般有3~5个节点组成,这样即使个别节点挂了,集群还可以正常运转。
Sentinel 负责监控主从节点的健康,当主节点挂掉时自动选择一个最优的从节点切换为主节点。当客户端来连接集群时会首先连接Sentinel,通过Sentinel来查询主节点的地址,然后再连接主节点进行数据交互。当主节点发生故障时,客户端会重新向Sentinel要地址,Sentinel会将最新的主节点地址告诉客户端,如此应用将无须重新启动就可以自动完成节点切换。
二、Redis Sentinel 故障转移
如上图所示,如果主节点挂掉了,原先的主从复制也断开了,一个从节点被提升为新的主节点,其他的从节点开始和新的主节点建立复制关系。客户端通过新的主节点继续进行交互,Sentinel会持续监听已经挂掉的主节点,待它恢变成从节点,从新的主节点哪里建立 复制关系。
故障转移可以概括为下面的几个步骤:
- 当多个sentinel发现master有问题会选举出一个sentinel为领导。
- 被选为领导的sentinel会选择出一个slave为新的master。
- 通知其余的slave成为新的master的slave。
- 通知客户端新的master的地址
- 等待原来的master复活成为新master的slave。
三、Redis Sentinel 配置与安装
1. redis 主节点
2. redis从节点
3. sentinel配置
配置说明:
- port: 端口号
- dir:工作目录
- logfile:日志
- sentinel monitor mymaster 127.0.0.1 7000 2 :mymaster代表的是master的名字,127.0.0.1 7000代表的是master的ip和端口,2代表的是至少有多少个sentinel发现master是有问题的才会进行下一步的故障转移
- sentinel down-after-milliseconds mymaster 3000:sentinel对master的进行一个判断,如果超过规定的时间master还没有响应就代表master是有问题的,这里是30000毫秒。
- sentinel parallel-syncs mymaster 1:老的slave对新的master复制同时是串行的还是并发的,这里的1 代表每次只能复制一个。
- sentinel failover-timeout mymastere 180000:故障转移的时间
四、使用客户端连接redis sentinel
1. 客户端实现基本原理
a. 客户端遍历sentinel节点集合获取一个可用的sentinel节点
b. 对可用sentinel获取master节点的名字、地址和端口
c. 获取到master节点后使用role/role application验证是否是真正的master
d. 当master发生变化时通知客户端
2.jedis
public class TestSentinel {
@SuppressWarnings("resource")
@Test
public void testSentinel() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10);
jedisPoolConfig.setMaxIdle(5);
jedisPoolConfig.setMinIdle(5);
// 哨兵信息
Set<String> sentinels = new HashSet<>(Arrays.asList("192.168.11.128:26379",
"192.168.11.129:26379","192.168.11.130:26379"));
// 创建连接池
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels,jedisPoolConfig,"123456");
// 获取客户端
Jedis jedis = pool.getResource();
// 执行两个命令
jedis.set("mykey", "myvalue");
String value = jedis.get("mykey");
System.out.println(value);
}
}
3. 通过spring进行配置
<bean id = "poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大空闲数 -->
<property name="maxIdle" value="50"></property>
<!-- 最大连接数 -->
<property name="maxTotal" value="100"></property>
<!-- 最大等待时间 -->
<property name="maxWaitMillis" value="20000"></property>
</bean>
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<constructor-arg name="poolConfig" ref="poolConfig"></constructor-arg>
<constructor-arg name="sentinelConfig" ref="sentinelConfig"></constructor-arg>
<property name="password" value="123456"></property>
</bean>
<!-- JDK序列化器 -->
<bean id="jdkSerializationRedisSerializer" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"></bean>
<!-- String序列化器 -->
<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory"></property>
<property name="keySerializer" ref="stringRedisSerializer"></property>
<property name="defaultSerializer" ref="stringRedisSerializer"></property>
<property name="valueSerializer" ref="jdkSerializationRedisSerializer"></property>
</bean>
<!-- 哨兵配置 -->
<bean id="sentinelConfig" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
<!-- 服务名称 -->
<property name="master">
<bean class="org.springframework.data.redis.connection.RedisNode">
<property name="name" value="mymaster"></property>
</bean>
</property>
<!-- 哨兵服务IP和端口 -->
<property name="sentinels">
<set>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="192.168.11.128"></constructor-arg>
<constructor-arg name="port" value="26379"></constructor-arg>
</bean>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="192.168.11.129"></constructor-arg>
<constructor-arg name="port" value="26379"></constructor-arg>
</bean>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="192.168.11.130"></constructor-arg>
<constructor-arg name="port" value="26379"></constructor-arg>
</bean>
</set>
</property>
</bean>
五、三个定时任务
为了保证可以为redis进行故障转移,redis sentinel内部有三个定时任务
1. 每10秒每个sentinel对master和slave执行info
这个定时任务的主要目的如下:
- 发现slave节点:在配置中并没有配置slave节点的信息,实际上是不需要的配置的 ,sentinel会对master执行info操作,会在info replication 中发现slave信息从而发现slave节点。
- 确认主从关系:对每个执行info就可以确认他们之前的关系和关系的变化。
2.每2秒每个sentinel通过master节点的channel交换信息(pub/sub)
mster节点上会有一个发布订阅的channel(频道)用于让sentinel节点进行信息交换利用的原理就是每个sentinel发布信息,其他的sentinel可以收到信息(信息中会包含当前自身的信息、master的信息和对master和slave的判断),最后sentinel之间会达成一个共识,他们之前的信息交换是利用的master的 channel,利用的是发布订阅。
这个定时任务的主要目的如下:
- 通过_sentinel_:hello频道交互
- 交互对节点的看法和自身的信息
3.每1秒每个sentinel对其他的sentinel执行和redis执行ping
这个定时任务的主要目的如下:
- 心跳检测,失败判断的依据
六、其他
1. 主观下线和可观下线
- 主观下线:每个sentinel节点对节点失败的偏见
- 客观下线:所有sentinel对节点的失败达成共识
2.领导者选择
- 原因:只有一个sentinel完成故障转移
- 选举:通过sentinel is-master-down-by-addr 命令都希望成为领导者
选举过程如下:
- 每个主观下线的sentinel向其他sentinel发送命令,要求将他设置为领导者。
- 收到命令的sentinel节点如果没有同意通过其他sentinel节点发送的命令,那么将同意该请求否则拒绝。
- 如果该sentinel节点发现自己已经超过sentinel集合的半数并且超过 sentinel monitor这个配置中所设置的数量,那么它将成为领导者。
- 如果此过程有多个sentinel成为领导者,那么将等待一段时间再次选举。
3.故障转移
故障转移有如下几个步骤:
- 从slave节点选出一个“合适”的节点作为新的matser
- 对上面的slave节点执行 slaveof no one 命令让其成为master节点。
- 向剩余的slave节点发送命令,让他们成为新master节点的slave节点,复制规则和parallel-sync参数有关
- 更新对原来master节点配置为slave,并保持着对其关注,当其恢复后命令它去复制新的master节点。
什么是合适的slave节点?
- 选择 save-priority(slave节点优先级)最高的slave节点,如果存在则返回,不存在则继续。
- 选择复制偏移量最大的slave节点(复制的最完整),如果存在则返回,不存在则继续。
- 选择runId最小的slave节点
4.消息丢失
Redis主从采用异步复制,意味着主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息就丢失了,如果主从延迟特别大,那么丢失的数据就可能会特别多。Sentinel无法保证数据不丢失,但也能保证消息少丢失,它有两个选项可以限制主从延迟过大。
min-slaves-to-write 1
min-slaves-max-lag 10
第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,丧失可用性。
第二个参数控制什么是正常复制,什么是异常复制,它的单位是秒(s),表示如果在10s没有收到从节点反馈,就意味着从节点同步不正常,要么是网络断开了,要么是一直没有给反馈。