方案介绍
首先对redis 高可用方案sentinel作一些介绍。redis的基本使用以及各种概念原理内容比较多,本文不作展开。推荐一本书籍,本文中的内容也主要是从这本书中了解学习得来。
**《Redis开发与运维》 付磊 张益军编著 **
redis sentinel是redis官方提供的高可用方案。主要功能是基于redis的主从复制基础上,提供节点故障检测,主节点选举,故障切换等等功能。保证服务不会因为单个redis节点宕机而出现故障。
先介绍一下主从复制,主从复制即将多个redis服务划分为主节点和从节点,主节点负责写数据以及将新增的数据同步给所属的从节点。
主从复制的基本原理就是首次复制时,主节点保存一份rdb数据文件,发送给从节点,从节点读取当中的数据进行数据同步。rdb文件之后的文件主节点保持一个缓冲区,每次增加,从缓冲区发送给从节点进行增量同步。
redis的主从复制拓扑结构有一主一从,一主多从,树状主从。这里我们讨论常用的一主多从结构。结构图如下:
redis维持节点的主从关系只要在从节点上使用如下配置便可以指定主节点。
slaveof {masterIP} {masterPort}
masterauth {masterPassword}
断开主从关系则在从节点上执行如下命令
slaveof no one
在主从复制的模式下,主节点的数据不断同步给从节点。当主节点出现故障,无法提供服务时,可以寻找一个从节点切换成主节点继续提供数据读写的服务。并且调整各个节点的主从关系,将从节点置为主节点,其余从节点指向的主节点也相应的作出调整。
将从节点切换成主节点以及调整主从拓扑关系的操作比较复杂,依赖手工调整耗时长并且不及时。因此redis官方提供了sentinel组件,将这一过程自动化。
部署流程
接下来使用docker容器部署三个redis以及sentinel节点来说明redis的主从复制和故障转移实现。
redis使用一主二从的主从复制结构。sentinel节点部署3个节点。sentinel的数量为3是有原因的。这是因为认定主节点宕机和选取新的主节点时需要一半以上(不包括一半)的sentinel节点参与。因此3个节点是最小的避免单点部署的数量。
所有节点的部署都使用docker,各个节点详情如下:
角色 | 端口 | 说明 |
---|---|---|
master | 6380 | redis主节点 |
slave-1 | 6381 | redis从节点1 |
slave-2 | 6382 | redis从节点2 |
sentinel-1 | 26380 | sentinel节点1 |
sentinel-2 | 26381 | sentinel节点2 |
sentinel-3 | 26382 | sentinel节点3 |
拓扑结构如下:
![](/home/markfengfeng/下载/未命名文件 (1).png)
主从复制部署
首先部署redis节点,并且配置好主从关系。
根据主从配置的方式我们修改三份redis的配置文件。slave-1, slace-2是master节点的从节点。通过slaveof {masterip} {masterport}
配置指定master作为主节点,其中关键的配置项如下:
master节点,master.conf
#绑定的IP
bind 0.0.0.0
#绑定端口
port 6380
#密码设置
requirepass password
masterauth password
slave-1节点,slave-1.conf
bind 0.0.0.0
port 6381
requirepass password
#master节点的IP和端口
slaveof 172.17.0.1 6380
#master节点的密码
masterauth password
slave-2节点,slave-2.conf
bind 0.0.0.0
requirepass password
port 6382
slaveof 172.17.0.1 6380
masterauth password
docker部署命令如下,配置文件映射到docker容器中:
docker run --name master -v /data/redis/master.conf:/usr/local/etc/redis/redis.conf -p 6380:6380 -d 192.168.168.98:5000/redis redis-server /usr/local/etc/redis/redis.conf
docker run --name slave-1 -v /data/redis/slave-1.conf:/usr/local/etc/redis/redis.conf -p 6381:6381 -d 192.168.168.98:5000/redis redis-server /usr/local/etc/redis/redis.conf
docker run --name slave-2 -v /data/redis/slave-2.conf:/usr/local/etc/redis/redis.conf -p 6382:6382 -d 192.168.168.98:5000/redis redis-server /usr/local/etc/redis/redis.conf
**注意事项 **
-
所有的节点密码配置一样,不一样不能够正常切换,因为sentinel要配置主节点密码。
-
主节点也要配置masterauth,因为当前主节点在经历故障和恢复后可能会被切换成从节点。
-
redis端口和docker映射的端口要保持一致。主节点在寻找从节点的时候是根据从节点的redis端口来寻找的,端口不一致,会找不到从节点。
-
配置文件中的一项
rename-command CONFIG "Sstarnetpbx"
要注释掉,会影响到后面sentinel切换故障节点。 -
为了保证数据的一致性,从节点不提供写功能,配置文件中要添加一项
slave-read-only yes
,一般默认配置项会有,不需要更改。
部署完之后可以通过info replication
命令查看redis的主从服务信息。
主节点
markfengfeng@markfengfeng:~$ redis-cli -p 6380 -a password6380
127.0.0.1:6380> info replication
# Replication
role:master #说明节点的角色为主节点
connected_slaves:2 #连接了两个从节点
slave0:ip=172.17.0.1,port=6381,state=online,offset=28,lag=1 #从节点1地址信息,状态,复制偏移量,延时
slave1:ip=172.17.0.1,port=6382,state=online,offset=28,lag=1 #从节点2地址信息,状态,复制偏移量,延时
master_replid:73e9036c293329e91c87f719794d0095438c6154
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28
两个从节点
markfengfeng@markfengfeng:~$ redis-cli -p 6381 -a password6381
127.0.0.1:6381> info replication
# Replication
role:slave #说明节点的角色为从节点
master_host:172.17.0.1 #主节点地址
master_port:6380 #主节点端口
master_link_status:up #主节点连接状态
master_last_io_seconds_ago:8
master_sync_in_progress:0
slave_repl_offset:1652
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:73e9036c293329e91c87f719794d0095438c6154
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1652
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1652
127.0.0.1:6381> exit
markfengfeng@markfengfeng:~$ redis-cli -p 6382 -a password6382
127.0.0.1:6382> info replication
# Replication
role:slave
master_host:172.17.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:5
master_sync_in_progress:0
slave_repl_offset:1708
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:73e9036c293329e91c87f719794d0095438c6154
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1708
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:15
repl_backlog_histlen:1694
通过主从节点的复制信息可以看出主从节点配置成功。
主从复制测试
测试一下主从同步的功能。
在主节点上执行添加数据命令
markfengfeng@markfengfeng:~$ redis-cli -p 6380 -a password6380
127.0.0.1:6380> set name xiehuafeng
OK
127.0.0.1:6380> get name
"xiehuafeng"
127.0.0.1:6380>
在从节点上查看数据
markfengfeng@markfengfeng:~$ redis-cli -p 6381 -a password6381
127.0.0.1:6381> get name
"xiehuafeng"
127.0.0.1:6381> exit
markfengfeng@markfengfeng:~$ redis-cli -p 6382 -a password6382
127.0.0.1:6382> get name
"xiehuafeng"
可以看出主节点的数据能够同步到从节点上。
sentinel部署
接下来部署sentinel节点。同样使用docker容器来部署。
虽然部署三个节点,但是除了端口不同外,其他配置都是一样的。配置redis节点地址都使用主节点地址。redis本身使用Gossip协议,通过master节点,将master节点上的从节点信息和sentinel节点信息提供给各个sentinel。
配置如下:
sentinel-1节点
port 26380 #sentinel的端口
sentinel monitor master 172.17.0.1 6380 2 #检查主redis节点的地址和端口信息,2代表至少两个sentinel节点认为主节点宕机才算宕机
sentinel auth-pass master password #主节点的密码
sentinel down-after-milliseconds master 30000 #主节点失联30秒后,认为主节点不可达
sentinel parallel-syncs master 1 #切换新的主节点后,从节点要从新的主节点同步数据
sentinel-2节点
port 26381
sentinel monitor master 172.17.0.1 6380 2
sentinel auth-pass master password
sentinel down-after-milliseconds master 30000
sentinel parallel-syncs master 1
sentinel-3节点
port 26382
sentinel monitor master 172.17.0.1 6380 2
sentinel auth-pass master password
sentinel down-after-milliseconds master 30000
sentinel parallel-syncs master 1
docker部署命令如下,我的docker镜像从docker hub中下载。它将所有的配置项作为容器的环境变量配置。因此上述的配置项,都在docker部署命令中的环境变量中体现。
docker run --name sentinel-1 -p 26380:26380 -e SENTINEL_PORT=26380 -e MASTER_NAME=master -e QUORUM=2 -e DOWN_AFTER=30000 -e PARALLEL_SYNCS=1 -e MASTER=172.17.0.1:6380 -e AUTH_PASS=password -d s7anley/redis-sentinel-docker
docker run --name sentinel-2 -p 26381:26381 -e SENTINEL_PORT=26381 -e MASTER_NAME=master -e QUORUM=2 -e DOWN_AFTER=30000 -e PARALLEL_SYNCS=1 -e MASTER=172.17.0.1:6380 -e AUTH_PASS=password -d s7anley/redis-sentinel-docker
docker run --name sentinel-3 -p 26382:26382 -e SENTINEL_PORT=26382 -e MASTER_NAME=master -e QUORUM=2 -e DOWN_AFTER=30000 -e PARALLEL_SYNCS=1 -e MASTER=172.17.0.1:6380 -e AUTH_PASS=password -d s7anley/redis-sentinel-docker
这里拿出其中一个节点的部署命令解释一下各个环境变量的含义
docker run --name sentinel-1 -p 26380:26380
-e SENTINEL_PORT=26380 # sentinel端口配置26380
-e MASTER_NAME=master #主节点的别名
-e QUORUM=2 #故障判定和选举的最小节点为2
-e DOWN_AFTER=30000 #30000毫秒后redis节点仍旧失联,则判定为下线
-e PARALLEL_SYNCS=1 #切换主节点并且调整主从拓扑结构后,重新同步主从数据,每次同步1个节点
-e MASTER=172.17.0.1:6380 #主节点的ip和地址
-e AUTH_PASS=password #主节点访问的密码
-d s7anley/redis-sentinel-docker
各个容器分别启动后,我们使用redis-cli客户端连接到其中一个sentinel节点中。使用info
命令查看连接详情
markfengfeng@markfengfeng:~$ redis-cli -p 26380
127.0.0.1:26380> info
# Server
redis_version:2.8.16
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:9b6f16e360929887
redis_mode:sentinel
os:Linux 4.15.0-45-generic x86_64
arch_bits:64
multiplexing_api:epoll
gcc_version:4.8.2
process_id:1
run_id:0c53be652240cc31135318c7f94bae86329b97bd
tcp_port:26380
uptime_in_seconds:258
uptime_in_days:0
hz:10
lru_clock:8899661
config_file:/etc/redis/sentinel.conf
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
master0:name=master,status=ok,address=172.17.0.1:6380,slaves=2,sentinels=3
#此处显示sentinel监控状态ok,master节点的ip,端口。并且能够显示从节点的数量和sentinel节点的数量
这种情况下sentinel对于redis节点的监控就是成功的了。
故障切换测试
接下来我们测试当主节点宕机后,sentinel是否能够自动进行故障切换。
先打开sentinel的日志,然后docker stop master
来手动关闭主节点
1:X 14 Mar 14:53:41.237 # +sdown master master 172.17.0.1 6380
1:X 14 Mar 14:53:41.325 # +new-epoch 1
1:X 14 Mar 14:53:41.333 # +vote-for-leader 06b9bedb039ec5cd119e2ea24ae02f3513e7a4f8 1
1:X 14 Mar 14:53:41.333 # +odown master master 172.17.0.1 6380 #quorum 2/2
1:X 14 Mar 14:53:41.333 # Next failover delay: I will not start a failover before Thu Mar 14 14:59:42 2019
1:X 14 Mar 14:53:42.538 # +config-update-from sentinel 06b9bedb039ec5cd119e2ea24ae02f3513e7a4f8 172.17.0.5 26381 @ master 172.17.0.1 6380
1:X 14 Mar 14:53:42.538 # +switch-master master 172.17.0.1 6380 172.17.0.1 6381
#此处显示切换6381为主节点
1:X 14 Mar 14:53:42.538 * +slave slave 172.17.0.1:6382 172.17.0.1 6382 @ master 172.17.0.1 6381 6382置为6381的从节点
查看6381的主从信息如下,6381成为主节点,而6382成为其从节点。
markfengfeng@markfengfeng:~$ redis-cli -p 6381 -a password
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=172.17.0.1,port=6382,state=online,offset=74435,lag=1
master_replid:7be531aa99fa3f3b73effb05823a273163c7b078
master_replid2:67b5a57405c7a737ba461f2ee9d949909014f7ac
master_repl_offset:74701
second_repl_offset:10923
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:74701
使用docker start master
命令启动master节点
1:X 14 Mar 15:00:05.866 * +convert-to-slave slave 172.17.0.1:6380 172.17.0.1 6380 @ master 172.17.0.1 6381
将6380重新切换到6381的从节点。再看6381的主从信息如下,可以看见6380成为其从节点
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=172.17.0.1,port=6382,state=online,offset=113285,lag=1
slave1:ip=172.17.0.1,port=6380,state=online,offset=113418,lag=1
master_replid:7be531aa99fa3f3b73effb05823a273163c7b078
master_replid2:67b5a57405c7a737ba461f2ee9d949909014f7ac
master_repl_offset:113418
second_repl_offset:10923
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:113418
sentinel的配置就结束了。
客户端连接
接下来讲解一下客户端程序怎么通过sentinel连接redis数据库。sentinel自动切换的时候而不影响客户端程序的执行。
基本原理
简单来说,已知sentinel管理着各个节点,并且知道当前的主节点是谁。那么把sentinel的节点配置给客户端的程序,客户端通过sentinel获取可用的主节点连接。当中细节的相关实现,redis提供的客户端程序包已经实现。下面给出一些示例。
java程序连接
java程序通过sentinel连接使用redis的简单示例。
先引入redis的jar包
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.2</version>
</dependency>
</dependencies>
java程序如下
public class JedisSentinelDemo {
private static JedisSentinelPool pool = null;
public void initPool() {
//将所有的sentinel节点地址端口信息加入set
Set<String> sentinels = new HashSet<String>();
sentinels.add("172.17.0.1:26380");
sentinels.add("172.17.0.1:26381");
sentinels.add("172.17.0.1:26382");
//创建一个默认的连接池的配置,连接池的相关配置以及优化可自行查看api
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
//创建sentinel的连接池,参数(主节点别名,sentinel节点集合,连接池配置, 连接超时时间,redis节点连接密码)
pool = new JedisSentinelPool("master", sentinels, config, 10000, "password");
}
public static void main(String[] args) {
JedisSentinelDemo demo = new JedisSentinelDemo();
demo.initPool();
Jedis jedis = null;
try {
int incre = 1;
while (true) {
Thread.currentThread().sleep(10000);
jedis = pool.getResource();
jedis.set("number", String.valueOf(incre++));
String value = jedis.get("number");
System.out.println(value);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null)
jedis.close();
}
}
}
程序测试
执行一下程序,控制台结果
三月 18, 2019 11:10:29 下午 redis.clients.jedis.JedisSentinelPool initSentinels
信息: Trying to find master from available Sentinels...
三月 18, 2019 11:10:29 下午 redis.clients.jedis.JedisSentinelPool initSentinels
信息: Redis master running at 172.17.0.1:6380, starting Sentinel listeners...
三月 18, 2019 11:10:29 下午 redis.clients.jedis.JedisSentinelPool initPool
信息: Created JedisPool to master at 172.17.0.1:6380
1
2
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199)
at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
at redis.clients.jedis.Protocol.process(Protocol.java:147)
at redis.clients.jedis.Protocol.read(Protocol.java:211)
at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:297)
at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:196)
at redis.clients.jedis.Jedis.set(Jedis.java:69)
at test.JedisSentinelDemo.main(JedisSentinelDemo.java:35)
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
at redis.clients.util.Pool.getResource(Pool.java:53)
at redis.clients.jedis.JedisSentinelPool.getResource(JedisSentinelPool.java:209)
at test.JedisSentinelDemo.main(JedisSentinelDemo.java:34)
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.ConnectException: 拒绝连接 (Connection refused)
at redis.clients.jedis.Connection.connect(Connection.java:164)
at redis.clients.jedis.BinaryClient.connect(BinaryClient.java:80)
at redis.clients.jedis.BinaryJedis.connect(BinaryJedis.java:1677)
at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:87)
at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:868)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:435)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:363)
at redis.clients.util.Pool.getResource(Pool.java:49)
... 2 more
Caused by: java.net.ConnectException: 拒绝连接 (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at redis.clients.jedis.Connection.connect(Connection.java:158)
... 9 more
redis.clients.jedis.exceptions.JedisException: Could not return the resource to the pool
at redis.clients.util.Pool.returnBrokenResourceObject(Pool.java:103)
at redis.clients.jedis.JedisSentinelPool.returnBrokenResource(JedisSentinelPool.java:234)
at redis.clients.jedis.JedisSentinelPool.returnBrokenResource(JedisSentinelPool.java:17)
at redis.clients.jedis.Jedis.close(Jedis.java:3355)
at test.JedisSentinelDemo.main(JedisSentinelDemo.java:43)
Caused by: java.lang.IllegalStateException: Invalidated object not currently part of this pool
at org.apache.commons.pool2.impl.GenericObjectPool.invalidateObject(GenericObjectPool.java:640)
at redis.clients.util.Pool.returnBrokenResourceObject(Pool.java:101)
... 4 more
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
at redis.clients.util.Pool.getResource(Pool.java:53)
at redis.clients.jedis.JedisSentinelPool.getResource(JedisSentinelPool.java:209)
at test.JedisSentinelDemo.main(JedisSentinelDemo.java:34)
Caused by: redis.clients.jedis.exceptions.JedisConnectionException:
......此处省略部分异常输出
三月 18, 2019 11:11:30 下午 redis.clients.jedis.JedisSentinelPool initPool
信息: Created JedisPool to master at 172.17.0.1:6381
4
5
有一些连接池初始化的信息打印出来,可以看出一些基本的流程顺序。
- 通过可用的sentinel节点获取redis主节点
- 找到主节点的地址 172.17.0.1:6380,sentinel开始监听
- 创建主节点的连接池
- 最后执行redis的相关api,打印出我们set和get的结果
然后我们docker stop master
停掉主节点后,程序出现连接异常。
一段时间过后,sentinel切换主节点,这次通过sentinel获取到的主节点已经变成了 172.17.0.1:6381。
可知redis的节点故障切换,客户端程序是能够通过sentinel获取到正确的可用主节点的。
spring的配置
我们通常使用spring配置redis的连接池,这里同样给出一个spring配置的示例.
redis相关和spring相关的jar包导入
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xhf</groupId>
<artifactId>JedisSentinelSpring</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>JedisSentinelSpring</name>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.0.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.0.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.8.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.7.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.4</version>
</dependency>
</dependencies>
</project>
spring的配置,spring-redis.xml。相比redis的spring配置。不同之处在于
- 添加RedisSentinelConfiguration的配置
- 调整RedisFactory的配置,将sentinel的配置注入redisFactory。去掉redis的地址配置,因为客户端会通过sentinel获取redis连接。
<?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"
xmlns:cache="http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">
<!-- jedis连接池的配置 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig" >
<property name="maxIdle" value="10000" />
<property name="maxWaitMillis" value="10000" />
<property name="testOnBorrow" value="true" />
</bean >
<!-- sentinel连接的配置, 注入主节点别名,添加各个sentinel节点信息到sentinels节点中 -->
<bean id="redisSentinelConfiguration" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
<property name="master">
<bean class="org.springframework.data.redis.connection.RedisNode">
<property name="name" value="master">
</property>
</bean>
</property>
<property name="sentinels">
<set>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="172.17.0.1" />
<constructor-arg name="port" value="26380" />
</bean>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="172.17.0.1" />
<constructor-arg name="port" value="26381" />
</bean>
<bean class="org.springframework.data.redis.connection.RedisNode ">
<constructor-arg name="host" value="172.17.0.1" />
<constructor-arg name="port" value="26382" />
</bean>
</set>
</property>
</bean>
<!-- 配置redis连接生成器,主要将sentinel的配置和redis连接池配置作为构造函数参数注入。另外添加上主节点password的配置 -->
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" >
<property name="password" value="password" />
<property name="timeout" value="30000" ></property>
<constructor-arg name="sentinelConfig" ref="redisSentinelConfiguration"> </constructor-arg>
<constructor-arg name="poolConfig" ref="poolConfig"></constructor-arg>
</bean >
<!-- RedisTemplate配置,提供一些数据的序列化和反序列化配置 -->
<bean id="keySerializer" class="org.springframework.data.redis.serializer.GenericToStringSerializer">
<constructor-arg index="0" type="java.lang.Class" value="java.lang.Object" />
</bean>
<bean id="serializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer">
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<property name="defaultSerializer" ref="serializer" />
<property name="keySerializer" ref="keySerializer" />
<property name="hashKeySerializer" ref="keySerializer" />
</bean>
</beans>
简单的java程序如下
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-redis.xml");
RedisTemplate<String, String> template = (RedisTemplate<String, String>) context.getBean("redisTemplate");
int incre = 0;
while (true) {
try {
Thread.currentThread().sleep(5000);
template.opsForValue().set("number", String.valueOf(incre++));
String city = template.opsForValue().get("number");
System.out.println(city);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
程序测试
执行程序,并且在中途停止主节点模拟主节点宕机的情况,控制台的结果如下:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
三月 18, 2019 12:36:50 下午 redis.clients.jedis.JedisSentinelPool initSentinels
信息: Trying to find master from available Sentinels...
三月 18, 2019 12:36:50 下午 redis.clients.jedis.JedisSentinelPool initSentinels
信息: Redis master running at 172.17.0.1:6380, starting Sentinel listeners...
三月 18, 2019 12:36:50 下午 redis.clients.jedis.JedisSentinelPool initPool
信息: Created JedisPool to master at 172.17.0.1:6380
0
1
2
3
4
5
6
7
8
9
org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:198)
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:345)
at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:129)
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:92)
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:79)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:191)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:166)
at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:88)
at org.springframework.data.redis.core.DefaultValueOperations.set(DefaultValueOperations.java:169)
at Main.main(Main.java:12)
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
at redis.clients.util.Pool.getResource(Pool.java:53)
at redis.clients.jedis.JedisSentinelPool.getResource(JedisSentinelPool.java:209)
at redis.clients.jedis.JedisSentinelPool.getResource(JedisSentinelPool.java:17)
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:191)
... 9 more
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.ConnectException: 拒绝连接 (Connection refused)
at redis.clients.jedis.Connection.connect(Connection.java:164)
at redis.clients.jedis.BinaryClient.connect(BinaryClient.java:80)
at redis.clients.jedis.BinaryJedis.connect(BinaryJedis.java:1677)
at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:87)
at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:868)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:435)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:363)
at redis.clients.util.Pool.getResource(Pool.java:49)
... 12 more
Caused by: java.net.ConnectException: 拒绝连接 (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at redis.clients.jedis.Connection.connect(Connection.java:158)
... 19 more
...............此处省略一部分异常显示
三月 18, 2019 12:38:12 下午 redis.clients.jedis.JedisSentinelPool initPool
信息: Created JedisPool to master at 172.17.0.1:6381
三月 18, 2019 12:38:12 下午 redis.clients.jedis.JedisSentinelPool initPool
信息: Created JedisPool to master at 172.17.0.1:6381
16
17
可见开始连接的是6380节点,并且能够正常工作。之后主节点宕机之后,程序出现连接异常。在sentinel自动切换主节点之后,控制台打印出了主节点切换到了6382端口的节点,并且程序能够正常执行。
结论
以上是sentinel一个基础的拓扑结构的部署流程。通过实践操作可以看出sentinel能够对redis节点的故障作出自动检测和切换。并且对客户端屏蔽这些变化,简单客户端程序的处理。这在一定程度上保证了redis的高可用性。
但是还是会存在一些问题,比如客户端只能够对主节点进行读写,这使得服务器的性能受到单个节点的限制。为了解决这些问题redis提供了redis cluster的集群方案,后续研究研究我再将相关介绍整理发出来。