哨兵模式是基于主从模式做的一定变化,它能够为Redis提供了高可用性。在实际生产中,服务器难免不会遇到一些突发状况:服务器宕机,停电,硬件损坏等。这些情况一旦发生,其后果往往是不可估量的。而哨兵模式在一定程度上能够帮我们规避掉这些意外导致的灾难性后果。
1.哨兵模式原理分析
其实,哨兵模式的核心还是主从复制。而哨兵(sentinel)是一个独立的进程, 可以实现对 Redis 实例的监控、通知、自动故障转移。借助哨兵来监控 master 节点的状态,如果master节点异常,则会做主从切换,将某一台slave作为master。
哨兵(sentinel)是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。
- 哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过 sentinel代理访问redis的主节点。
- 当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis 主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息) 。
是如何知道某个节点不可用的?
每个哨兵节点每秒通过 ping 去进行心跳监测(包括所有redis实例和sentinel同伴),并根据回复判断节点是否在线。
如果某个 sentinel 线程发现主库没有在给定时间( down-after-milliseconds)内响应这个PING,则这个 sentinel 线程认为主库是不可用的,这种情况叫 主观失效(SDOWN)。这种情况一般不会引起马上的故障自动转移。
但是当多个 sentinel 线程确实发现主库是不可用并超过 sentinel.conf 里面的配置项 sentinel monitor mymaster {#ip} {#port} {#number} 中的#number时候(这里实际上采用了流言协议),其余 sentinel 线程会通过 RAFT 算法推举领导的 sentinel 线程负责主库的客观下线并同时负责故障自动转移,这种情况叫 客观失效(ODOWN)。
优点:
- 监控:它会监听主服务器和从服务器之间是否在正常工作。
- 通知:它能够通过API告诉系统管理员或者程序,集群中某个实例出了问题。
- 故障转移:它在主节点出了问题的情况下,会在所有的从节点中竞选出一个节点,并将其作为新的主节点。
- 提供主服务器地址:它还能够向使用者提供当前主节点的地址。这在故障转移后,使用者不用做任何修改就可以知道当前主节点地址。
缺点:
- 哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间存在访问瞬断的情况
- 哨兵模式只有一个主节点对外提供服务,没法支持很高的并发
- 单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率
2.哨兵模式搭建
2.1 redis哨兵搭建步骤
1、复制一份sentinel.conf文件
cp sentinel.conf sentinel‐26379
2、将相关配置修改为如下值:
port 26379
daemonize yes
pidfile "/var/run/redis‐sentinel‐26379.pid"
logfile "26379.log"
dir "/usr/local/redis‐5.0.3/data"
# sentinel monitor <master‐name> <ip> <redis‐port> <quorum>
# quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/2 + 1),master才算真正失效)
sentinel monitor mymaster 192.168.0.60 6379 2
3、启动sentinel哨兵实例
src/redis‐sentinel sentinel‐26379.conf
4、查看sentinel的info信息
src/redis‐cli ‐p 26379
127.0.0.1:26379>info
可以看到Sentinel的info里已经识别出了redis的主从
5、可以自己再配置两个sentinel,端口26380和26381,注意上述配置文件里的对应数字都要修改
public class JedisSentinelTest {
public static void main(String[] args) throws IOException {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPoolConfig.setMaxIdle(5);
String masterName = "mymaster";
Set<String> sentinels = new HashSet<>();
sentinels.add(new HostAndPort("39.105.132.120", 26379).toString());
sentinels.add(new HostAndPort("39.105.132.120", 26380).toString());
sentinels.add(new HostAndPort("39.105.132.120", 26381).toString());
// JedisSentinelPool其实本质与JedisPool类似,都是与redis主节点建立的连接池
// JedisSentinelPool并不是说与sentinel建立的连接池,而是通过sentinel发现redis主节点,并与其建立连接
JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, jedisPoolConfig, 3000, null);
Jedis jedis = jedisSentinelPool.getResource();
System.out.println(jedis.set("sentinel", "lisi"));
System.out.println(jedis.get("sentinel"));
// 这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池
jedis.close();
}
}
2.3 基于SpringBoot
引入相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐data‐redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons‐pool2</artifactId>
</dependency>
springboot项目核心配置:
server:
port: 8080
spring:
redis:
database: 0
timeout: 3000
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认为8
max-active: 8
# 连接池中的最大空闲连接 默认为8
max-idle: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1
max-wait: -1ms
# 连接池中的最小空闲连接 默认为 0
min-idle: 0
#哨兵模式
sentinel:
# 主节点的别名
master: mymaster
# sentinel服务的ip和端口
nodes: 43.107.136.120:26379,43.107.136.120:26380,43.107.136.120:26381
访问代码:
@RestController
@RequestMapping("/redis")
public class RedisController {
// 使用SpringBoot封装的RestTemplate对象
@Autowired
RedisTemplate<String, String> redisTemplate;
/**
* 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到
* 新的master选举出来后,哨兵会把消息发布出去,客户端实际上是实现了一个消息监听机制,
* 当哨兵把新master的消息发布出去,客户端会立马感知到新master的信息,从而动态切换访问的master ip
*/
@RequestMapping("/get")
public void testSentinel() throws Exception {
int i = 1;
while(true) {
redisTemplate.opsForValue().set("test"+i, i+"");
System.out.println("设置key:" + "test" + i);
i++;
Thread.sleep(1000);
}
}
@RequestMapping("/get")
public String get(String key) {
String value = redisTemplate.opsForValue().get(key);
return value;
}
@RequestMapping("/set")
public String set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
return "success";
}
}