1. 哨兵

在一个一主多从的 Redis 系统中,主 Redis 主要负责写请求以及将数据同步给从 Redis,从 Redis 可以接受客户端的读请求以及接受主 Redis 同步过来的数据。当主 Redis 异常不能正常提供服务时,可以手动选择一个从 Redis 并将其升级为主 Redis,以使得整个系统能正常对外提供服务。当原来的主 Redis 恢复之后,再手动将其降级为从 Redis 加入系统当中。这整个过程需要人工的介入,不能实现自动化。

哨兵是 Redis 的高可用解决方案。它可以监控 Redis 系统的运行状况,当主 Redis 出现故障时,可以进行故障自动转移,它将选择一个从 Redis 并将其自动升级为主 Redis,当原来的主 Redis 恢复时,会自动将其降级为从 Redis 重新加入 Redis 系统当中。这整个过程由哨兵来完成,无需人工的介入。

2. 部署概况

角色 IP 端口
master
(主)
10.10.10.127 6379
slave
(从)
10.10.10.128 6379
slave
(从)
10.10.10.129 6379
sentinel
(哨兵)
10.10.10.127 26379
sentinel
(哨兵)
10.10.10.128 26379
sentinel
(哨兵)
10.10.10.129 26379

3. redis 一主多从部署

此处省去 Redis 的安装步骤,具体可参考另一篇文章:Redis安装和应用

10.10.10.127(master)redis.conf 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 是否以守护进程的方式运行
daemonize yes
#
# 端口
port 6379
#
# 当redis作为守护进程运行时,它会将pid写入到该参数指定的文件里面
pidfile /usr/local/redis/run/redis.pid
#
# 文件保存到磁盘的快照文件的目录路径
dir /usr/local/redis/data
#
# 日志文件
logfile /usr/local/redis/logs/redis.log

10.10.10.128(slave)redis.conf 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 是否以守护进程的方式运行
daemonize yes
#
# 端口
port 6379
#
# 当redis作为守护进程运行时,它会将pid写入到该参数指定的文件里面
pidfile /usr/local/redis/run/redis.pid
#
# 文件保存到磁盘的快照文件的目录路径
dir /usr/local/redis/data
#
# 日志文件
logfile /usr/local/redis/logs/redis.log
#
# 配置该项说明当前服务是一个slave节点。它会根据配置从master节点进行数据同步
slaveof 10.10.10.127 6379

与主配置不同的是,多了一个slaveof配置项,表明这是一个从数据库。

10.10.10.129(slave)redis.conf 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 是否以守护进程的方式运行
daemonize yes
#
# 端口
port 6379
#
# 当redis作为守护进程运行时,它会将pid写入到该参数指定的文件里面
pidfile /usr/local/redis/run/redis.pid
#
# 文件保存到磁盘的快照文件的目录路径
dir /usr/local/redis/data
#
# 日志文件
logfile /usr/local/redis/logs/redis.log
#
# 配置该项说明当前服务是一个slave节点。它会根据配置从master节点进行数据同步
slaveof 10.10.10.127 6379

与主配置不同的是,多了一个slaveof配置项,表明这是一个从数据库。

最后,分别启动这几台服务器上的 Redis 服务:

1
# redis-server /usr/local/redis/redis.conf

4. 哨兵集群

哨兵本质上是一个特殊的 Redis 服务,不需要额外安装其它应用来支持。

10.10.10.127(sentinel)sentinel.conf 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 是否以守护进程的方式运行
daemonize yes
#
# 端口
port 26379
#
# 当redis作为守护进程运行时,它会将pid写入到该参数指定的文件里面
pidfile /usr/local/redis/run/sentinel.pid
#
# 文件保存到磁盘的快照文件的目录路径
dir /usr/local/redis/data
#
# 日志文件
logfile /usr/local/redis/logs/sentinel.log
#
# 配置哨兵监听redis的master节点
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
# <master-name>:自定义master节点的名称
# <ip>:master节点的ip地址
# <redis-port>:master节点的端口号
# <quorum>:当有quorum个哨兵主观认为master节点失效时,才会认为master节点客观上真正失效
sentinel monitor mymaster 10.10.10.127 6379 2
#
# 哨兵连接redis的master节点的密码(没有设置密码可以不配,如果配置了,那么redis主从节点都应配置相同的密码)
sentinel auth-pass mymaster 654321
#
# 哨兵会周期性的PING主节点(master),如果master节点在规定时间内没有回应,哨兵就会主观上认为master节点失效了。默认是30秒
sentinel down-after-milliseconds mymaster 30000
#
# 当发生failover(故障转移)进行主从切换时,有多少个salve对新的master节点进行同步
# 如果slave节点用来做查询,应尽量配置小的值,避免在故障转移期间所有的slave因进行同步而不可用
sentinel parallel-syncs mymaster 1
#
# 故障转移超时时间
sentinel failover-timeout mymaster 180000

10.10.10.12810.10.10.129上的sentinel.conf配置与上同。

哨兵可以通过监控主 Redis 自动发现复制主 Redis 的从 Redis,以及自动发现监控主 Redis 的其它哨兵。哨兵会周期性的发送PING给主和从 Redis,以监控 Redis 系统的运行状况,主和从 Redis 如果不能在规定时间内应答哨兵,则哨兵会主观认为 Redis 服务下线。

最后,分别启动这几台服务器上的哨兵服务:

1
# redis-sentinel /usr/local/redis/sentinel.conf --sentinel

5. 简单应用

# pom.xml


1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class RedisSentinelDemoTest {
private JedisSentinelPool jedisSentinelPool;
@Before
public void doBefore() {
// 链接池配置
JedisPoolConfig poolConfig = new JedisPoolConfig();
// 当没有可用链接时是否阻塞,直到超时
poolConfig.setBlockWhenExhausted(true);
// 最大空闲链接数
poolConfig.setMaxIdle(8);
// 最小空闲链接数
poolConfig.setMinIdle(4);
// 最大链接数
poolConfig.setMaxTotal(8);
// 最大等待时间
poolConfig.setMaxWaitMillis(30000);
// 获取链接时是否检查可用性
poolConfig.setTestOnBorrow(true);
// 链接归还池时是否检查可用性
poolConfig.setTestOnReturn(true);
// 哨兵
Set<String> sentinels = new HashSet<>();
sentinels.add("10.10.10.127:26379");
sentinels.add("10.10.10.128:26379");
sentinels.add("10.10.10.129:26379");
// 链接池
jedisSentinelPool = new JedisSentinelPool("mymaster", sentinels, poolConfig, 5000);
}
@Test
public void doTest() {
Jedis jedis = jedisSentinelPool.getResource();
if (jedis.exists("myname")) {
System.out.println("=====> " + jedis.get("myname"));
} else {
jedis.set("myname", "fanlychie");
}
jedis.close();
}
}

6. 与Spring集成

# pom.xml


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.18.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.18.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

redis 参数配置:

# redis.properties


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# master 名称
redis.masterName = mymaster
# 哨兵 1
redis.sentinel1 = 10.10.10.127:26379
# 哨兵 2
redis.sentinel2 = 10.10.10.128:26379
# 哨兵 3
redis.sentinel3 = 10.10.10.129:26379
# 当没有可用链接时是否阻塞,直到超时
redis.blockWhenExhausted = true
# 最大空闲链接数
redis.maxIdle = 8
# 最小空闲链接数
redis.minIdle = 4
# 最大链接数
redis.maxTotal = 8
# 最大等待时间
redis.maxWaitMillis = 30000
# 获取链接时是否检查可用性
redis.testOnBorrow = true
# 链接归还池时是否检查可用性
redis.testOnReturn = true

spring 配置:

# spring-context.xml


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:redis.properties</value>
</list>
</property>
</bean>
<context:annotation-config/>
<context:component-scan base-package="org.fanlychie"/>
<!-- redis 链接池配置 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="blockWhenExhausted" value="${redis.blockWhenExhausted}"/>
<property name="maxIdle" value="${redis.maxIdle}"/>
<property name="minIdle" value="${redis.minIdle}"/>
<property name="maxTotal" value="${redis.maxTotal}"/>
<property name="maxWaitMillis" value="${redis.maxWaitMillis}"/>
<property name="testOnBorrow" value="${redis.testOnBorrow}"/>
<property name="testOnReturn" value="${redis.testOnReturn}"/>
</bean>
<!-- redis 哨兵配置 -->
<bean id="sentinelConfig" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
<constructor-arg name="master" value="${redis.masterName}"/>
<constructor-arg name="sentinelHostAndPorts">
<set>
<value>${redis.sentinel1}</value>
<value>${redis.sentinel2}</value>
<value>${redis.sentinel3}</value>
</set>
</constructor-arg>
</bean>
<!-- redis 链接配置 -->
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<constructor-arg name="poolConfig" ref="poolConfig"/>
<constructor-arg name="sentinelConfig" ref="sentinelConfig"/>
</bean>
<!-- redis 字符串序列化 -->
<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
<!-- redis 对象转为json串序列化 -->
<bean id="jsonRedisSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer">
<constructor-arg name="mapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper"/>
</constructor-arg>
</bean>
<!-- redis 模板 -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="keySerializer" ref="stringRedisSerializer"/>
<property name="valueSerializer" ref="jsonRedisSerializer"/>
<property name="hashKeySerializer" ref="stringRedisSerializer"/>
<property name="hashValueSerializer" ref="jsonRedisSerializer"/>
</bean>
</beans>

单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/spring-context.xml")
public class RedisSentinelSpringDemoTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void doTest() {
if (redisTemplate.hasKey("myinfo")) {
System.out.println("=====> " + redisTemplate.opsForValue().get("myinfo"));
} else {
Map<String, Object> map = new HashMap<>();
map.put("name", "fanlychie");
map.put("mail", "fanlychie@yeah.net");
redisTemplate.opsForValue().set("myinfo", map);
}
}
}