前面的两篇文章(Redis的持久化方案一文掌握Redis的三种集群方案)分别介绍了Redis的持久化与集群方案 —— 包括主从复制模式、哨兵模式、Cluster模式,其中主从复制模式由于不能自动做故障转移,当节点出现故障时需要人为干预,不满足生产环境的高可用需求,所以在生产环境一般使用哨兵模式或Cluster模式。那么在Spring Boot项目中,如何访问这两种模式的Redis集群,可能遇到哪些问题,是本文即将介绍的内容。

Spring Boot 2 整合Redis

spring boot中整合Redis非常简单,在pom.xml中添加依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

spring boot 2的spring-boot-starter-data-redis中,默认使用的是lettuce作为redis客户端,它与jedis的主要区别如下:

  1. Jedis是同步的,不支持异步,Jedis客户端实例不是线程安全的,需要每个线程一个Jedis实例,所以一般通过连接池来使用Jedis
  2. Lettuce是基于Netty框架的事件驱动的Redis客户端,其方法调用是异步的,Lettuce的API也是线程安全的,所以多个线程可以操作单个Lettuce连接来完成各种操作,同时Lettuce也支持连接池

如果不使用默认的lettuce,使用jedis的话,可以排除lettuce的依赖,手动加入jedis依赖,配置如下

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

在配置文件application.yml中添加配置(针对单实例)

spring:
redis:
host: 192.168.40.201
port: 6379
password: passw0rd
database: 0 # 数据库索引,默认0
timeout: 5000 # 连接超时,单位ms
jedis: # 或lettuce, 连接池配置,springboot2.0中使用jedis或者lettuce配置连接池,默认为lettuce连接池
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池分配连接最大阻塞等待时间(阻塞时间到,抛出异常。使用负值表示无限期阻塞)
max-idle: 8 # 连接池中的最大空闲连接数
min-idle: 0 # 连接池中的最小空闲连接数

然后添加配置类。其中@EnableCaching注解是为了使@Cacheable、@CacheEvict、@CachePut、@Caching注解生效

@Configuration
@EnableCaching
public class RedisConfig { @Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory); // 使用Jackson2JsonRedisSerialize 替换默认的jdkSerializeable序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

上述配置类注入了自定义的RedisTemplate<String, Object>, 替换RedisAutoConfiguration中自动配置的RedisTemplate<Object, Object>类(RedisAutoConfiguration另外还自动配置了StringRedisTemplate)。

此时,我们可以通过定义一个基于RedisTemplate的工具类,或通过在Service层添加@Cacheable、@CacheEvict、@CachePut、@Caching注解来使用缓存。比如定义一个RedisService类,封装常用的Redis操作方法,

@Component
@Slf4j
public class RedisService { @Autowired
private RedisTemplate<String, Object> redisTemplate; /**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
log.error("exception when expire key {}. ", key, e);
return false;
}
} /**
* 根据key获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
} /**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
log.error("exception when check key {}. ", key, e);
return false;
}
} ...
}

出于篇幅,完整代码请查阅本文示例源码: https://github.com/ronwxy/springboot-demos/tree/master/springboot-redis-sentinel

或在Service层使用注解,如

@Service
@CacheConfig(cacheNames = "users")
public class UserService { private static Map<String, User> userMap = new HashMap<>(); @CachePut(key = "#user.username")
public User addUser(User user){
user.setUid(UUID.randomUUID().toString());
System.out.println("add user: " + user);
userMap.put(user.getUsername(), user);
return user;
} @Caching(put = {
@CachePut( key = "#user.username"),
@CachePut( key = "#user.uid")
})
public User addUser2(User user) {
user.setUid(UUID.randomUUID().toString());
System.out.println("add user2: " + user);
userMap.put(user.getUsername(), user);
return user;
}
...
}

Spring Boot 2 整合Redis哨兵模式

Spring Boot 2 整合Redis哨兵模式除了配置稍有差异,其它与整合单实例模式类似,配置示例为

spring:
redis:
password: passw0rd
timeout: 5000
sentinel:
master: mymaster
nodes: 192.168.40.201:26379,192.168.40.201:36379,192.168.40.201:46379 # 哨兵的IP:Port列表
jedis: # 或lettuce
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0

完整示例可查阅源码: https://github.com/ronwxy/springboot-demos/tree/master/springboot-redis-sentinel

上述配置只指定了哨兵节点的地址与master的名称,但Redis客户端最终访问操作的是master节点,那么Redis客户端是如何获取master节点的地址,并在发生故障转移时,如何自动切换master地址的呢?我们以Jedis连接池为例,通过源码来揭开其内部实现的神秘面纱。

在 JedisSentinelPool 类的构造函数中,对连接池做了初始化,如下

 public JedisSentinelPool(String masterName, Set<String> sentinels,
final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
final String password, final int database, final String clientName) {
this.poolConfig = poolConfig;
this.connectionTimeout = connectionTimeout;
this.soTimeout = soTimeout;
this.password = password;
this.database = database;
this.clientName = clientName; HostAndPort master = initSentinels(sentinels, masterName);
initPool(master);
} private HostAndPort initSentinels(Set<String> sentinels, final String masterName) { for (String sentinel : sentinels) {
final HostAndPort hap = HostAndPort.parseString(sentinel); log.fine("Connecting to Sentinel " + hap); Jedis jedis = null;
try {
jedis = new Jedis(hap.getHost(), hap.getPort()); List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName); // connected to sentinel...
sentinelAvailable = true; if (masterAddr == null || masterAddr.size() != 2) {
log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
+ ".");
continue;
} master = toHostAndPort(masterAddr);
log.fine("Found Redis master at " + master);
break;
} catch (JedisException e) {
// resolves #1036, it should handle JedisException there's another chance
// of raising JedisDataException
log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e
+ ". Trying next one.");
} finally {
if (jedis != null) {
jedis.close();
}
}
}
//省略了非关键代码 for (String sentinel : sentinels) {
final HostAndPort hap = HostAndPort.parseString(sentinel);
MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
// whether MasterListener threads are alive or not, process can be stopped
masterListener.setDaemon(true);
masterListeners.add(masterListener);
masterListener.start();
} return master;
}

initSentinels 方法中主要干了两件事:

  1. 遍历哨兵节点,通过get-master-addr-by-name命令获取master节点的地址信息,找到了就退出循环。get-master-addr-by-name命令执行结果如下所示
[root@dev-server-1 master-slave]# redis-cli -p 26379
127.0.0.1:26379> sentinel get-master-addr-by-name mymaster
1) "192.168.40.201"
2) "7001"
127.0.0.1:26379>
  1. 对每一个哨兵节点通过一个 MasterListener 进行监听(Redis的发布订阅功能),订阅哨兵节点+switch-master频道,当发生故障转移时,客户端能收到哨兵的通知,通过重新初始化连接池,完成主节点的切换。

    MasterListener.run方法中监听哨兵部分代码如下
 j.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
log.fine("Sentinel " + host + ":" + port + " published: " + message + "."); String[] switchMasterMsg = message.split(" "); if (switchMasterMsg.length > 3) { if (masterName.equals(switchMasterMsg[0])) {
initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
} else {
log.fine("Ignoring message on +switch-master for master name "
+ switchMasterMsg[0] + ", our master name is " + masterName);
} } else {
log.severe("Invalid message received on Sentinel " + host + ":" + port
+ " on channel +switch-master: " + message);
}
}
}, "+switch-master");

initPool 方法如下:如果发现新的master节点与当前的master不同,则重新初始化。

private void initPool(HostAndPort master) {
if (!master.equals(currentHostMaster)) {
currentHostMaster = master;
if (factory == null) {
factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
soTimeout, password, database, clientName, false, null, null, null);
initPool(poolConfig, factory);
} else {
factory.setHostAndPort(currentHostMaster);
// although we clear the pool, we still have to check the
// returned object
// in getResource, this call only clears idle instances, not
// borrowed instances
internalPool.clear();
} log.info("Created JedisPool to master at " + master);
}
}

通过以上两步,Jedis客户端在只知道哨兵地址的情况下便能获得master节点的地址信息,并且当发生故障转移时能自动切换到新的master节点地址。

Spring Boot 2 整合Redis Cluster模式

Spring Boot 2 整合Redis Cluster模式除了配置稍有差异,其它与整合单实例模式也类似,配置示例为

spring:
redis:
password: passw0rd
timeout: 5000
database: 0
cluster:
nodes: 192.168.40.201:7100,192.168.40.201:7200,192.168.40.201:7300,192.168.40.201:7400,192.168.40.201:7500,192.168.40.201:7600
max-redirects: 3 # 重定向的最大次数
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0

完整示例可查阅源码: https://github.com/ronwxy/springboot-demos/tree/master/springboot-redis-cluster

一文掌握Redis的三种集群方案 中已经介绍了Cluster模式访问的基本原理,可以通过任意节点跳转到目标节点执行命令,上面配置中 max-redirects 控制在集群中跳转的最大次数。

查看JedisClusterConnection的execute方法,

public Object execute(String command, byte[]... args) {

    Assert.notNull(command, "Command must not be null!");
Assert.notNull(args, "Args must not be null!"); return clusterCommandExecutor
.executeCommandOnArbitraryNode((JedisClusterCommandCallback<Object>) client -> JedisClientUtils.execute(command,
EMPTY_2D_BYTE_ARRAY, args, () -> client))
.getValue();
}

集群命令的执行是通过ClusterCommandExecutor.executeCommandOnArbitraryNode来实现的,

public <T> NodeResult<T> executeCommandOnArbitraryNode(ClusterCommandCallback<?, T> cmd) {

    Assert.notNull(cmd, "ClusterCommandCallback must not be null!");
List<RedisClusterNode> nodes = new ArrayList<>(getClusterTopology().getActiveNodes());
return executeCommandOnSingleNode(cmd, nodes.get(new Random().nextInt(nodes.size())));
} private <S, T> NodeResult<T> executeCommandOnSingleNode(ClusterCommandCallback<S, T> cmd, RedisClusterNode node,
int redirectCount) { Assert.notNull(cmd, "ClusterCommandCallback must not be null!");
Assert.notNull(node, "RedisClusterNode must not be null!"); if (redirectCount > maxRedirects) {
throw new TooManyClusterRedirectionsException(String.format(
"Cannot follow Cluster Redirects over more than %s legs. Please consider increasing the number of redirects to follow. Current value is: %s.",
redirectCount, maxRedirects));
} RedisClusterNode nodeToUse = lookupNode(node); S client = this.resourceProvider.getResourceForSpecificNode(nodeToUse);
Assert.notNull(client, "Could not acquire resource for node. Is your cluster info up to date?"); try {
return new NodeResult<>(node, cmd.doInCluster(client));
} catch (RuntimeException ex) { RuntimeException translatedException = convertToDataAccessException(ex);
if (translatedException instanceof ClusterRedirectException) {
ClusterRedirectException cre = (ClusterRedirectException) translatedException;
return executeCommandOnSingleNode(cmd,
topologyProvider.getTopology().lookup(cre.getTargetHost(), cre.getTargetPort()), redirectCount + 1);
} else {
throw translatedException != null ? translatedException : ex;
}
} finally {
this.resourceProvider.returnResourceForSpecificNode(nodeToUse, client);
}
}

上述代码逻辑如下

  1. 从集群节点列表中随机选择一个节点
  2. 从该节点获取一个客户端连接(如果配置了连接池,从连接池中获取),执行命令
  3. 如果抛出ClusterRedirectException异常,则跳转到返回的目标节点上执行
  4. 如果跳转次数大于配置的值 max-redirects, 则抛出TooManyClusterRedirectionsException异常

可能遇到的问题

  1. Redis连接超时

检查服务是否正常启动(比如 ps -ef|grep redis查看进程,netstat -ano|grep 6379查看端口是否起来,以及日志文件),如果正常启动,则查看Redis服务器是否开启防火墙,关闭防火墙或配置通行端口。

  1. Cluster模式下,报连接到127.0.0.1被拒绝错误,如 Connection refused: no further information: /127.0.0.1:7600

这是因为在redis.conf中配置 bind 0.0.0.0bind 127.0.0.1导致,需要改为具体在外部可访问的IP,如 bind 192.168.40.201。如果之前已经起了集群,并产生了数据,则修改redis.conf文件后,还需要修改cluster-config-file文件,将127.0.0.1替换为bind 的具体IP,然后重启。

  1. master挂了,slave升级成为master,重启master,不能正常同步新的master数据

如果设置了密码,需要在master, slave的配置文件中都配置masterauth password

相关阅读:

  1. Redis的持久化方案
  2. 一文掌握Redis的三种集群方案

作者:空山新雨,一枚仍在学习路上的IT老兵

近期作者写了几十篇技术博客,内容包括Java、Spring Boot、Spring Cloud、Docker,技术管理心得等

欢迎关注作者微信公众号:空山新雨的技术空间,一起学习成长

Spring Boot(十三):整合Redis哨兵,集群模式实践的更多相关文章

  1. Redis进阶实践之十 Redis哨兵集群模式

    一.引言             上一篇文章我们详细的讲解了Redis的主从集群模式,其实这个集群模式配置很简单,只需要在Slave的节点上进行配置,Master主节点的配置不需要做任何更改,但是有一 ...

  2. 单台服务器-利用docker搭建Redis哨兵集群模式

    前言:只有一台华为云服务器,所以打算创建三个容器来模拟三个服务器了. 一:拉取redis镜像 二:拉取redis.conf文件 放在自定义的目录下:wget -c http://download.re ...

  3. Linux - redis哨兵集群实例

    目录 Linux - redis哨兵集群实例 命令整理 配置流程 Linux - redis哨兵集群实例 命令整理 官网地址:http://redisdoc.com/ redis-cli info # ...

  4. helm部署Redis哨兵集群

    介绍 Redis Sentinel集群是由若干Sentinel节点组成的分布式集群,可以实现故障发现.故障自动转移.配置中心和客户端通知. 如下图: Redis Sentinel 故障转移过程: 从这 ...

  5. 11.Redis 哨兵集群实现高可用

    作者:中华石杉 Redis 哨兵集群实现高可用 哨兵的介绍 sentinel,中文名是哨兵.哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能: 集群监控:负责监控 redis mast ...

  6. python连接redis哨兵集群

    一.redis集群模式有多种, 哨兵模式只是其中的一种实现方式, 其原理请自行谷歌或者百度 二.python 连接 redis 哨兵集群 1. 安装redis包 pip install redis 2 ...

  7. redis哨兵集群搭建

    下载redis jar包redis-4.0.11.tar.gz放在/data/redis目录下 解压 命令:tar -zxvf redis-4.0.11.tar.gz 解压后如图所示 在/usr/lo ...

  8. redis哨兵集群+spring boot 2.×

    Ubuntu集群构建篇 redis-cli:不跟参数,默认访问localhost:6379端口,无密码登陆 redis-cli -h ${host} -p ${port} -a ${password} ...

  9. redis 哨兵集群原理及部署

    复制粘贴自: https://www.cnblogs.com/kevingrace/p/9004460.html 请点击此链接查看原文. 仅供本人学习参考, 如有侵权, 请联系删除, 多谢! Redi ...

随机推荐

  1. LeetCode43(字符串相乘)

    题目: 给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式. 示例 1: 输入: num1 = "2", ...

  2. Object.defineProperty注意事项

    Object.defineProperty() 方法设置属性时,属性不能同时声明访问器属性( set 和 get )和 writable 或者 value 属性. 意思就是,某个属性设置了 writa ...

  3. AndroidStudio实现AIDL

    AIDL的使用步骤 aidl远程调用传递的参数和返回值支持Java的基本类型(int long booen char byte等)和String,List,Map等.当然也支持一个自定义对象的传递. ...

  4. 网络地址转换NAT与端口地址转换PAT

    网络地址转换NAT与端口地址转换PAT 一.网络地址转换 NAT (Network Address Translation) 1.1.网络地址转换简介 需要在专用网(内网)连接到因特网的路由器上安装 ...

  5. 达拉草201771010105《面向对象程序设计(java)》第十三周学习总结

    达拉草201771010105<面向对象程序设计(java)>第十三周学习总结 第一部分:理论知识 事件处理基础: 事件源:能够产生事件的对象都可 以成为事件源,如文本框.按钮等.一个事件 ...

  6. python画一颗拳头大的💗

    用上turtle库后,各种画,今天画个拳头大的爱心@.@. 下面贴下代码: # -*- coding: utf-8 -*- # Nola import pygame import time impor ...

  7. 前端面试题(HTML、CSS部分)

    HTML.CSS部分: 一.html5有哪些新特性.移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5?   新特性: HTML5 现在已经不是 SGML 的 ...

  8. node生成excel,动态替换表格内容

    这里使用的是exceljs模块, 好上手,易操作 1. 大致使用步骤 npm install exceljs // 引用var Excel = require('exceljs'); // 创建一个w ...

  9. 前端开发--nginx篇

    安装和启动 Mac上搭建nginx教程 通过Homebrew 安装nginx brew install nginx 配置 添加配置文件在 /usr/local/etc/nginx/servers 目录 ...

  10. vuex的使用心得

    今天的工作内容-----vuex的使用心得: 都知道,对于小型的项目来说不必使用vuex,但是对于需要把共享的变量全部存储在一个对象里面,然后把这个对象放在顶层组件中以供其他组件使用.其实vuex就是 ...