【微服务】springboot整合redis哨兵集群使用详解
目录
一、前言
对于大多数开发的同学来说,redis再熟悉不过了,基本上来说,在一个微服务项目中,redis几乎成了标配,经验来看,redis大多数作为缓存来使用,而且使用起来学习成本可以说很低了。通常来说,为了确保redis的高可用性,生产环境夏一般会使用集群模式,这个需要结合项目自身的情况选择,比如你的项目主要是为了应对高并发读,主从集群即可满足,而如果你的项目不仅读写频繁,而且需要存储的缓存数量也很大,可能cluster集群模式更适合你。本篇将以redis的哨兵集群为例,从搭建到与springboot的整合做详细的说明。
二、环境准备
基于centos7的虚拟机,或云服务器一台(至少一台)。
三、安装redis
3.1 前置准备
3.1.1 下载安装包
选择合适的版本进行下载,下载地址:Index of /releases/1604emgaMTkzMDcyNTg1NC4xNjY3ODkyODY2ga_8BKGRQKRPV*MTY4NzMxMzg1OC43LjEuMTY4NzMxMzg4NS4zMy4wLjA.
3.1.2 准备依赖环境
执行命令:yum install -y gcc tcl
3.1.3 上传并解压包
tar -zxvf redis-6.2.11
cd redis-6.2.11
3.2 执行安装
进入到解压后的主目录,执行下面的命令
make && make install
看到下面的效果,说明安装成功
四、搭建redis主从集群
4.1 环境准备
我们知道,哨兵集群的目的是为了监控主从集群中的master节点的状态,一旦master节点挂掉了,可以迅速选出一个新的主节点,从而坐到自动故障切换,所以需要先搭建一个主从集群,规划如下
机器地址 | 端口 | 角色 |
192.168.9.131 | 7001 | master |
192.168.9.131 | 7002 | slave |
192.168.9.131 | 7003 | slave |
4.2 搭建过程
4.2.1 创建实例文件目录
在主目录下创建3个文件夹,分别为7001,7002,7003,文件名称可以自定,这里是为了方便区分多个实例,通过端口号的形式命名;
mkdir 7001 7002 7003
4.2.2 修改redis.conf配置文件
备份一下原始的redis主目录中的redis.conf文件没然后编辑redis.conf文件,修改下面两行配置
bind 0.0.0.0
protected-mode no #本地测试验证吗,暂时关掉包含模式
4.2.3 拷贝配置文件
从redis的主目录中拷贝redis.conf文件分别到7001,7002,7003中
cp redis.conf ./7001
cp redis.conf ./7002
cp redis.conf ./7003
4.2.4 修改配置文件端口信息
由于这里是单机,为了区分多个实例,以端口来区分,分别进入到3个目录下,将端口号修分别修改为 7001,7002,7003,主要修改里面的端口号,依次修改为7001,7002,7003,其他的配置暂时不做修改;
也可以通过下面的命令进行批量修改
sed -i -e 's/6379/7001/g' -e 's/dir .\//dir \//usr/local/redis/7001\//g' slave1/redis.conf
sed -i -e 's/6379/7001/g' -e 's/dir .\//dir \//usr/local/redis/7002\//g' slave2/redis.conf
sed -i -e 's/6379/7001/g' -e 's/dir .\//dir \//usr/local/redis/7003\//g' slave3/redis.conf
4.2.5 修改声明的IP地址
虚拟机本身存在多个IP,为了避免将来混乱,需要在redis.conf文件中指定每一个实例的绑定ip信息,格式如下:
replica-announce-ip 当前IP
仍然可以使用批量修改的方式进行编辑
sed -i '1a replica-announce-ip 120.26.108.145' 7001/redis.conf
sed -i '1a replica-announce-ip 120.26.108.145' 7002/redis.conf
sed -i '1a replica-announce-ip 120.26.108.145' 7003/redis.conf
4.2.6 启动redis实例
上面的配置就完成了,在主目录下执行下面的命令依次启动3个redis实例,我这里使用的是后台启动,也可以直接前台启动,去掉nohup即可;
nohup redis-server /usr/local/redis/7001/redis.conf &
nohup redis-server /usr/local/redis/7002/redis.conf &
nohup redis-server /usr/local/redis/7003/redis.conf &
通过ps查看进程,可以看到3个实例都已经起来了
4.2.7 开启主从关系
上面启动了3个实例,但是他们之间还并没有形成主从关系,要配置主从可以使用replicaof 或者slaveof(5.0以前)命令。
有临时和永久两种模式:
- 修改配置文件(永久生效),在redis.conf中添加一行配置: slaveof <masterip> <masterport>;
- 使用redis-cli客户端连接到redis服务,执行slaveof命令(重启后失效): slaveof <masterip> <masterport>;
这里为了演示看出效果,我们采用第二种方式进行说明,在任意的shelli窗口,执行redis-cli命令连接7002,执行下面的命令:
redis-cli -p 7002
然后通过命令:info repliaction可以检查当前实例的身份
在另一个窗口连接7003这个实例客户端,使用相同的方式操作即可,到这里,一主两从的主从集群就搭建好了,当然也可以验证下效果,比如在从节点的命令行中,set一个key,可以看到下面的效果;
五、搭建redis哨兵集群
基于上述已经搭建好的主从集群模式下,开始搭建哨兵集群,关于哨兵集群的原理相关的知识,有兴趣的同学可以参考相关的资料,网上比较丰富;
5.1 添加哨兵配置文件
5.1.1 在三个目录下添加配置文件
在7001,7002,7003三个目录下,分别添加一个 sentinel.conf的文件,以7001目录的该配置为例,将下面的配置拷贝进去
port 27001
bind 0.0.0.0 #云服务测试的时候建议这样配置
sentinel announce-ip #"你的IP"
sentinel monitor mymaster #你的IP 7001 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
dir "/usr/local/redis/7001"
关于上述配置算是极简版的,对各项内容简单说明一下:
-
port 27001
:是当前sentinel实例的端口 -
sentinel monitor mymaster IP 7001 2
:指定主节点信息 -
mymaster
:主节点名称,自定义,任意写 -
IP 7001
:主节点的ip和端口 -
2
:选举master时的quorum值
5.1.2 拷贝配置文件
然后将7001目录下的该配置文件依次拷贝到其他3个目录下,拷贝过去之后,注意修改下面两个地方,即端口号和dir的位置,本次哨兵的三个端口为:27001,27002,27003;
5.2 启动哨兵集群
在当前主目录下依次执行下面的3行命令,启动3个哨兵
redis-sentinel 7001/sentinel.conf
redis-sentinel 7002/sentinel.conf
redis-sentinel 7003/sentinel.conf
3个哨兵启动后效果依次如下,可以看到各自监听的端口;
5.3 故障模拟
下面将主节点7001的redis实例对应的进程kill掉,然后看看哨兵控制台的日志信息变化如何
5.3.1 哨兵控制台日志
kill掉7001的实例之后,通过控制台日志,可以捕获到sentinel的关键日志信息,但是每个sentinel的日志信息稍有差异,从上到小,分别为监控的7001~7003的三个redis实例的sentinel日志信息;
关于里面的日志内容,有兴趣的同学可以参阅相关的资料进行深入的学习和解读,这些日志的输出其实也就是redis哨兵集群进行master节点选举的完整流程;
5.4 故障恢复
通过上述的命令再次开启7001的实例;
5.4.1 哨兵控制台日志
再次启动7001的实例后,不难发现,此时被sentinel集群监控到了,但是此时只能作为一个slave的角色加入到集群中,如下展示了三个sentinel实例监控时的日志信息,从上到下,分别为监控的7001~7003的三个redis实例的sentinel日志信息;
如果我们再次使用redis-cli命令登录到7002的客户端,使用info命令查看一下,可以看到此时的7002已经成为master节点,这个与sentinel中输出的日志信息也是吻合的;
六、springboot整合redis哨兵集群
搭建完成了哨兵集群后,接下来演示下如何在微服务中整合使用。
6.1 前置准备
6.1.1 搭建一个springboot工程
完整的工程目录如下
6.1.2 引入核心依赖
引入必须的jar,其他的可以根据自身情况引入
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
6.2 核心代码
6.2.1 哨兵配置类
添加一个哨兵配置类,用于配置哨兵相关的信息
package com.congge.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import redis.clients.jedis.JedisPoolConfig;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Configuration
@EnableAutoConfiguration
public class RedisSentinelConfig {
private static Logger logger = LoggerFactory.getLogger(RedisSentinelConfig.class);
@Value("#{'${spring.redis.sentinel.nodes}'.split(',')}")
private List<String> nodes;
@Value("${spring.redis.sentinel.nodes}")
private String redisNodes;
@Value("${spring.redis.sentinel.master}")
private String master;
//redis的连接池
@Bean(name = "poolConfig")
@ConfigurationProperties(prefix = "spring.redis")
public JedisPoolConfig poolConfig() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
return poolConfig;
}
@Bean
public RedisSentinelConfiguration sentinelConfiguration() {
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
//配置matser的名称
redisSentinelConfiguration.master(master);
//数据库是1库
redisSentinelConfiguration.setDatabase(1);
//配置redis的哨兵sentinel
Set<RedisNode> redisNodeSet = new HashSet<>();
nodes.forEach(x -> {
redisNodeSet.add(new RedisNode(x.split(":")[0], Integer.parseInt(x.split(":")[1])));
});
logger.info("redisNodeSet -->" + redisNodeSet);
redisSentinelConfiguration.setSentinels(redisNodeSet);
return redisSentinelConfiguration;
}
@Bean("redisConnectionFactory")
public JedisConnectionFactory redisConnectionFactory(
JedisPoolConfig poolConfig,
RedisSentinelConfiguration sentinelConfig) {
return new JedisConnectionFactory(sentinelConfig, poolConfig);
}
}
6.2.2 redistemplate配置类
在该配置类中针对ke/value进行序列化相关设置,非必须,如果不设置将会使用java默认的序列化;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
/**
* 方法描述: 初始化redis连接
* @param redisConnectionFactory redis连接工厂
* @return {@link RedisTemplate}
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 新建redisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置工厂
template.setConnectionFactory(redisConnectionFactory);
//序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//1,用StringRedisSerializer进行序列化的值,在Java和Redis中保存的内容是一样的
//2,用Jackson2JsonRedisSerializer进行序列化的值,在Redis中保存的内容,比Java中多了一对双引号。
//3,用JdkSerializationRedisSerializer进行序列化的值,对于Key-Value的Value来说,是在Redis中是不可读的。对于Hash的Value来说,比Java的内容多了一些字符。
//如果Key的Serializer也用和Value相同的Serializer的话,在Redis中保存的内容和上面Value的差异是一样的,所以我们保存时,只用StringRedisSerializer进行序列化
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(stringRedisSerializer);
// 返回redisTemplate对象
return template;
}
}
6.2.3 添加测试接口
增加一个测试接口,测试在接口中操作哨兵集群
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisTemplate redisTemplate;
//localhost:8083/redis/setValue?key=address&value=hangzhou
@GetMapping("setValue")
public String setValue(String key,String value) {
redisTemplate.opsForValue().set(key, value);
return "true";
}
//localhost:8083/redis/getValue?key=address
@GetMapping("getValue")
public String getValue(String key) {
String value = (String) redisTemplate.opsForValue().get(key);
System.out.println(value);
return value;
}
}
6.2.4 接口正常效果测试
启动工程后,调用上面的接口
sentinel客户端窗口日志信息
浏览器中请求如下接口,向集群中插入一条数据
接口执行成功后,再执行查询接口,可以查到上述插入到集群中的key
同时可以登录redis的客户端,检查上述插入的key/value
6.2.5 接口异常效果测试一
将master节点进程强制kill掉,
kill掉master进程之后,集群存在一个短暂的重新选举的过程
然后触发重新选举master的过程
请求接口后控制台输出的日志,由于master被kill掉,会重新建立连接信息
如果在此期间继续向集群执行写入操作,将会存在短暂的不可用的过程,等到集群重新选出master节点之后,接口又可以重新写入数据了,而对于客户端来说,这个是无感知的,因为客户端并不关心数据写入到哪个节点上,从上面的选举来看,7002这个slave节点的实例被选举为主节点;
6.2.6 重新恢复之前的master节点
再次启动7001的redis实例后,sentinel集群会重新发起选举,7001不再是master,而是作为7002实例的slave节点加入集群;
选举完成后,可以再次请求接口执行数据写入
登录到7002的客户端,可以看到数据写入成功
6.2.7 接口异常效果测试二
在集群模拟异常测试过程中,出现过下面的错误,这里贴出异常信息,大概的意思是,客户端写redis的时候,连接到了集群的从节点,默认情况下,哨兵集群中的从节点是没有写数据权限的;
关于这个异常,网上也有一些同学遇到过,大致的解决方案如下:
- 配置sentinel.conf时,没有设置密码,所以需要在配置哨兵文件时增加密码的设置:sentinel auth-pass mymaster 123456;
- 如果是阿里云或其他云服务器,可能是安全组中sentinel的端口没有开放,需要开放响应的端口;
该问题也是小编在实际工作中遇到的一个问题,对于这个问题,我在上面的故障模拟中的分析结论如下:
- 网络延迟有点大,当网络延迟太大造成哨兵之间感知的时间超过了哨兵配置的故障转移时间,这种情况下,可能会造成选举时间过长而失败;
- Redis主节点出现网络故障,与哨兵节点失联,这种情况下,哨兵无法获取主节点的信息,因此无法对主节点进行健康检查,并在需要时执行故障转移操作;
- Redis哨兵节点自身故障,导致哨兵节点无法在集群中正常工作;
遇到上述问题的时候,为了尽量减少问题面的扩散,建议的做法是:
- 排查网络,确认哨兵所在机器的网络是否有问题;
- 检查redis集群自身的状况,看看主从集群的关系是否出现故障;
- 如果确认了前两步没什么问题的情况下,建议重启哨兵;
七、写在文末
哨兵集群是一种非常重要的redis集群模式,这是一种高可用集群的常用部署方式,有必要深入的学习并掌握,希望对看到的同学有用,本篇到此结束,感谢观看!
版权声明:
本文为[小码农叔叔]所创,转载请带上原文链接,感谢
https://blog.csdn.net/zhangcongyi420/article/details/131881510