redis哨兵

简介

哨兵:Sentinel,一个独立运行的进程,监控主从机器,在故障时自动转移

用途:让redis宕机后可以自动进行故障转移。用有一个或多个哨兵组成哨兵系统,来监视任意数量服务器(包括主机和主机下属的从机),并在被监视的主机故障下线时,自动将主机下的从机升级为主机。

分类:

  1. 单机哨兵
  2. 多哨兵

单哨兵工作过程:

  1. 哨兵发送命令给各个服务器

  2. 各个服务器响应并返回运行状态给哨兵

  3. 哨兵发现主机宕机,

    1. 就自动将slave切换成master
    2. 通过发布订阅模式告知其他服务器,修改配置文件,切换主机。

多哨兵模式:

  • 使用多个哨兵监控服务器。
  • 且各哨兵之间相互监控。
  • 一般是3个及以上。

多哨兵工作、故障切换:

  1. 主服务器宕机,哨兵1先检测到,但系统并不会马上进行failover过程,这是因为仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线
  2. 当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作
  3. 切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

哨兵结构

1606724867172

类型 ip地址 端口
redis服务器(主机) A p1
redis服务器(备机) B p1
哨兵1 A p2
哨兵2 B p2

首先,建立主备关系,确认从机隶属的主机。

其次,建立哨兵,确认哨兵监听的主机。

这样,当哨兵监听的主机下线时,会自动获取主机下的从机作为主机。

配置服务器

修改redis.conf文件。

主机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#开启保护模式
protected-mode yes
bind 0.0.0.0

# 是否以守护线程的形式运行,默认no
daemonize yes

# 端口,默认6379
port 6379

masterauth 123456
# 访问主机redis需要的密码
requirepass 123456

slave-read-only yes # 设置读写分离,备机只负责读
slave-priority 100

# 日志输出文件
logfile "/app/redis/log/redis.log"

从机

从机,比主机,多一个slaveof的配置和密码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected-mode yes
bind 0.0.0.0

# 是否以守护线程的形式运行,默认是no
daemonize yes

# 端口,默认6379
port 6379

# 日志输出文件
logfile "/app/redis/log/redis.log"

# 主机redis密码
masterauth 123456

# 对应的主机,5.0版本升级成relicaof
slave-read-only yes # 设置读写分离,备机只负责读
slave-priority 90
slaveof 10.14.6.70 6379

查看redis版本

./redis-server –version

./redis-server -v

./redis-cli –version

./redis-cli -v

配置哨兵

cd /app/redis/bin,发现有可执行文件 redis-sentinel

cd /app/redis/conf,手动增加并修改sentinel.conf文件。

多个哨兵分布在不同服务器上,sentinel.conf 相同。(也可多个哨兵分布在同一个服务器,端口不同)

复制到其他服务器 scp sentinel.conf 用户名@ip地址:/app/redis/conf/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
daemonize yes

# 和redis保持一致
protected-mode yes
bind 0.0.0.0

# 哨兵端口号,默认26379
port 26379

# 【被哨兵监控的主机】,不能写localhost或127.0.0.1
# sentinel monitor:代表监控
# mymaster:代表服务器的默认名称,可以自定义,此处为apnmaster
# 10.14.6.70:代表监控的主服务器IP地址,6379代表端口
# 1:代表只有1个或1个以上的哨兵认为主机故障时,才会进行故障转移(failover)操作
#sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor apnmaster 10.14.6.70 6379 1

# 日志
logfile "/app/redis/log/sentinel.log"

# 是否可达的轮询间隔,30000毫秒没收到回复则认为故障,默认30秒
sentinel down-after-milliseconds mymaster 30000

# 配置连接主机和备机的密码,主机和备机的密码要相同,因为哨兵不能分别为master和slave设置密码。
# sentinel author-pass:代表设置密码
# mymaster:主从机名称
# 123456:主从机密码
#sentinel auth-pass <master-name> <password>
sentinel auth-pass apnmaster 123456

启动并验证

配置和启动时候,一定要注意用户是否有权限启动,比如chmod 775 bin / chown want.want dump.rdb

如果启动不成功,需要查看日志原因 tailf -n 100 /app/redis/log/redis.log

1
2
3
4
5
6
7
cd /app/redis/bin

# 启动Redis服务器进程
./redis-server ../conf/redis.conf

# 启动哨兵进程
./redis-sentinel ../conf/sentinel.conf --sentinel

主从验证

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
./redis-cli -h localhost -p 6379 -a 密码

# 主机redis
localhost:6379> info replication
# Replication
role:master
connected_slaves:1 #有一个备机
slave0:ip=10.14.6.69,port=6379,state=online,offset=6569974,lag=0
master_repl_offset:6569974
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:5521399
repl_backlog_histlen:1048576

# 从机redis
localhost:6379> info replication
# Replication
role:slave # 角色是备机
master_host:10.14.6.70 #主机ip
master_port:6379
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:6570197
slave_priority:90
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:17
repl_backlog_histlen:0

主从恢复

SLAVEOF,用于在 Redis 运行时,动态地修改复制(replication)功能的行为。

  1. SLAVEOF host port : 将当前服务器(无论从、主),变为指定服务器host:port的从机。
    • 如果当前机器本是A主机的从机,那会变成指定的host:port主机的从机。当前机器【会丢弃】从A同步的数据集,全新同步新主机的数据。
    • 如果当前机器本是主机,那会变成指定的host:port主机的从机。当前机器【会丢弃】原来的数据集,全新同步新主机的数据。
  2. SLAVEOF NO ONE:将从机变为主机,且【不丢弃】原来同步的数据集。【故而主机故障时,从机变为主机,数据不丢失,实现了无间断运行】
1
2
3
4
5
6
7
8
# 当Redis运行时,突然主机故障,可以使用`SLAVEOF`命令,手动重设主备关系(而不需要重启redis)。
./redis-cli -h localhost -p 6379 -a 密码

#首先,设置为主机:
localhost:6379>SLAVEOF NO ONE

#其次,给备机重新指定主机:
localhost:6379>SLAVEOF 10.14.6.70 6379

读写分离验证

仅主机能写入,但都能读到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#查看当前所有key
localhost:6379> keys *

# 主机redis写
localhost:6379> set mytest a
OK
# 主机redis读
localhost:6379> get mytest
"a"

# 从机redis写(不可写)
localhost:6379> set mytest a
(error) READONLY You can't write against a read only slave.

# 从机redis读
localhost:6379> get mytest
"a"

哨兵验证

当主机A故障,从机B成为主机,此时主机A又恢复了,B仍然是主机,而不被A抢回。

哨兵信息查看

哨兵节点本质上是一个特殊的Redis节点,可以通过redis-cli查询相关信息。

1
2
3
4
5
6
7
8
9
> ./redis-cli -h 127.0.0.1 -p 26379 info Sentinel

# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=apnmaster,status=ok,address=10.14.6.70:6379,slaves=1,sentinels=2
验证哨兵自动恢复
  1. 启动所有redis
    启动所有哨兵
  2. 查看哨兵日志
    查看主机/从机的状态 info replication
  3. kill -9 主机进程ID
  4. 查看哨兵日志
    查看主机/从机的状态 info replication,发现重新选举了主机。
哨兵日志

跟踪日志: tailf -n 100 /app/redis/log

日志内容:

  1. sdown:主观宕机,一个哨兵,ping一个主机超时了,这个哨兵自己就主观认为宕机了。
  2. odown:客观宕机,一个哨兵在一定时间内,收到了满足一定数量的哨兵的消息,大家都觉得主机宕机了。
  3. new-epoch: 标注版本号

  1. try-failover:达到故障转移条件,发起转移,开始等待其他哨兵的选举(只有发起哨兵有这个)
  2. vote-for-leader: 所有哨兵投票
  3. selected-slave:选出一个备机当新主机
  4. switch-master apnmaster:进行身份切换
  5. convert-to-slave slave:进行身份切换
  6. failover-end:故障转移完成

spring boot中使用哨兵模式

maven依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</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
public void testSentinel() {
// 创建连接池
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10);
jedisPoolConfig.setMaxIdle(5);
jedisPoolConfig.setMinIdle(5);
// 哨兵模式,指定哨兵列表的ip:port
JedisSentinelPool pool = new JedisSentinelPool("apnmaster", new HashSet<>(Arrays.asList("10.14.6.70:26379", "10.14.6.69:26379")), jedisPoolConfig, 60000, "123456");
// 获取客户端
Jedis jedis = pool.getResource();

// 执行两个命令
jedis.set("testKey", "testValue");
String value = jedis.get("testKey");
System.out.println(value);

---------------------------------------------
// 非哨兵模式,指定redis的ip:port
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "10.14.6.70", 6379, 30000, "ShLsEs3j", 0);

// 获取客户端
Jedis jedis1 = jedisPool.getResource();
jedis1.set("testKey1", "testValue1");
String value1 = jedis.get("testKey1");
System.out.println(value1);
}

springboot调用

application.properties

增加spring.redis.sentinel相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#spring.redis.host=10.14.6.121 # 单机模式才需要
#spring.redis.port=6379 # 单机模式才需要
#redis.cache.host=redis://:abc@10.14.6.121:6379/1 # 单机模式才需要

#注意不要使用springboot的ENC加密
spring.redis.password=123456
spring.redis.pool.max-active=100
spring.redis.database=0

#------redis哨兵模式------------
# 哨兵监听的主机别名
spring.redis.sentinel.master=apnmaster
# 哨兵的部署节点
spring.redis.sentinel.nodes=10.14.6.70:26379,10.14.6.69:26379
方式1:springboot java 程序
读取application.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@ConfigurationProperties(prefix = "redis")
public class RedisClientProperties {
private int database = 0;
private String host = "localhost";
private String password;
private int port = 6379;
private int timeout = Protocol.DEFAULT_TIMEOUT;
private Pool pool;
private Sentinel sentinel;

public static class Pool {
private int maxIdle = 8;
private int minIdle = 0;
private int maxActive = 40;
private int maxWait = 5000;
}

public static class Sentinel {
private String master;
private String nodes;
}
}
程序调用
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
public class RedisClientAutoConfiguration {
@Autowired
private RedisClientProperties properties;

// 可以把jedisPool放到RedisCache中,这样就可以全局调用redisCache了
@Bean("redisCache")
public RedisCache RedisCache(@Qualifier("jedisPool") final Pool<Jedis> jedisPool) {
final RedisCache cache = new RedisCache();
cache.setJedisPool(jedisPool);
return cache;
}

// 根据conf参数进行模式选择
@Bean("jedisPool")
public Pool<Jedis> JedisPool() {
final Sentinel sentinel = properties.getSentinel();
if (sentinel != null && sentinel.getMaster() != null && sentinel.getNodes() != null){
return createRedisSentinelPool();//2.哨兵模式调用
} else {
return createRedisPool();//3.非哨兵调用
}
}


// 3.非哨兵调用
private Pool<Jedis> createRedisPool() {
return new JedisPool(jedisPoolConfig(), properties.getHost(), properties.getPort(), properties.getTimeout(),properties.getPassword(), properties.getDatabase());
}

}

// 2.哨兵模式调用
private Pool<Jedis> createRedisSentinelPool() {
final Sentinel sentinel = properties.getSentinel();

final String master = sentinel.getMaster();
final String nodes = sentinel.getNodes();
final String[] nodeArray = nodes.split(",");

final Set<String> sentinels = new HashSet<>();
for (final String node : nodeArray) {
sentinels.add(node.trim());
}

return new JedisSentinelPool(master, sentinels, jedisPoolConfig(), properties.getTimeout(),properties.getPassword(), properties.getDatabase());
}

// 1.连接池
private JedisPoolConfig jedisPoolConfig() {
final JedisPoolConfig config = new JedisPoolConfig();
final RedisClientProperties.Pool props = this.properties.getPool() != null ? this.properties.getPool() : new RedisClientProperties.Pool();
config.setMaxTotal(props.getMaxActive());
config.setMaxIdle(props.getMaxIdle());
config.setMinIdle(props.getMinIdle());
config.setMaxWaitMillis(props.getMaxWait());
return config;
}
}
方式2:springboot 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
<!-- pool注册到代码中使用,RedisCache参见下文 -->
<bean id="redisCache"
class="RedisCache">
<property name="jedisPool" ref="jedisSentinelPool" />
</bean>

<!-- 哨兵模式配置 -->
<bean id="jedisSentinelPool" class="redis.clients.jedis.JedisSentinelPool">
<constructor-arg index="0" value="${spring.redis.sentinel.master}" />
<constructor-arg index="1" value="#{'${spring.redis.sentinel.nodes}'.split(',')}"/>
<constructor-arg index="2" ref="jedisPoolConfig" />
<constructor-arg index="3" value="${spring.redis.password}"/>
</bean>

<!-- 连接池 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="${redis.pool.maxTotal:200}" />
<property name="maxIdle" value="${redis.pool.maxIdle:10}" />
<property name="timeBetweenEvictionRunsMillis" value="${redis.pool.timeBetweenEvictionRunsMillis:30000}" />
<property name="minEvictableIdleTimeMillis" value="${redis.pool.minEvictableIdleTimeMillis:1800000}" />
<property name="softMinEvictableIdleTimeMillis" value="${redis.pool.softMinEvictableIdleTimeMillis:1800000}" />
<property name="maxWaitMillis" value="${redis.pool.maxWaitMillis:1500}" />
<property name="testOnBorrow" value="${redis.pool.testOnBorrow:true}" />
<property name="testWhileIdle" value="${redis.pool.testWhileIdle:true}" />
<property name="testOnReturn" value="${redis.pool.testOnReturn:false}" />
<property name="blockWhenExhausted" value="${redis.pool.blockWhenExhausted:false}" />
</bean>


<!--------- 非哨兵模式配置 --------->
<bean id="jedisURI" class="java.net.URI">
<constructor-arg index="0" value="${redis.cache.host}"/>
</bean>

<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<constructor-arg index="0" ref="jedisPoolConfig" />
<constructor-arg index="1" ref="jedisURI" type="java.net.URI"/>
<constructor-arg index="2" value="${redis.client.timeout:2000}"/>
<constructor-arg index="3" value="${redis.client.timeout:2000}"/>
</bean>
使用示例
1
2
3
4
@Autowired
private RedisCache redisCache;

final Long incrValue = redisCache.incr(redisKey); // 使用举例
RedisCache封装常见redis操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface ICache {
String get(String key);
<T> List<T> getList(String key, Class<T> clazz);
<T> T get(String key, Class<T> clazz);
String set(String key, String value);
String set(String key, Object object);
String set(String key, String value, int seconds);
String set(String key, Object object, int seconds);
void del(String key);
Long ttl(String key);
Long incr(String key);
Long incr(String key, long start);
Long expireAt(String key, long unixTime);
Long expire(String key, int seconds);
Boolean exists(String key);
}
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
public class RedisCache implements ICache {
private Pool<Jedis> jedisPool;

private Jedis getResource() {
return jedisPool.getResource();
}

public void setJedisPool(final Pool<Jedis> jedisPool) {
this.jedisPool = jedisPool;
}

@Override
public String get(final String key) {
Jedis jedis = null;
try {
jedis = getResource();
return jedis.get(key);
} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public <T> List<T> getList(final String key, final Class<T> clazz) {
Jedis jedis = null;
try {
jedis = getResource();
final String value = jedis.get(key);
return JsonUtil.toList(value, clazz);
} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public <T> T get(final String key, final Class<T> clazz) {
Jedis jedis = null;
try {
jedis = getResource();
final String value = jedis.get(key);
return JsonUtil.toBean(value, clazz);
} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public String set(final String key, final String value) {
Jedis jedis = null;
try {
jedis = getResource();
return jedis.set(key, value);
} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public String set(final String key, final Object object) {
final String value = JsonUtil.toJson(object);
Jedis jedis = null;
try {
jedis = getResource();
return jedis.set(key, value);

} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public String set(final String key, final String value, final int seconds) {
Jedis jedis = null;
try {
jedis = getResource();
return jedis.setex(key, seconds, value);
} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public Long incr(final String key) {
Jedis jedis = null;
try {
jedis = getResource();
return jedis.incr(key);
} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public Long incr(final String key, final long start) {
Jedis jedis = null;
try {
jedis = getResource();
return jedis.incrBy(key, start);
} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public Boolean exists(final String key) {
Jedis jedis = null;
try {
jedis = getResource();
return jedis.exists(key);
} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public Long expireAt(final String key, final long unixTime) {
Jedis jedis = null;
try {
jedis = getResource();
return jedis.expireAt(key, unixTime);
} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public Long expire(final String key, final int seconds) {

Jedis jedis = null;

try {
jedis = getResource();
return jedis.expire(key, seconds);

} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public String set(final String key, final Object object, final int seconds) {
final String value = JsonUtil.toJson(object);
Jedis jedis = null;
try {
jedis = getResource();
return jedis.setex(key, seconds, value);

} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public void del(final String key) {
Jedis jedis = null;
try {
jedis = getResource();
jedis.del(key);
} finally {
if (jedis != null) {
jedis.close();
}
}
}

@Override
public Long ttl(final String key) {
Jedis jedis = null;
try {
jedis = getResource();
return jedis.ttl(key);
} finally {
if (jedis != null) {
jedis.close();
}
}
}

参考

https://www.cnblogs.com/guolianyu/p/10249687.html

-------------Keep It Simple Stupid-------------
0%