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

Spring Boot 2 整合Redis

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

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </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依赖,配置如下

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. <exclusions>
  5. <exclusion>
  6. <groupId>io.lettuce</groupId>
  7. <artifactId>lettuce-core</artifactId>
  8. </exclusion>
  9. </exclusions>
  10. </dependency>
  11. <dependency>
  12. <groupId>redis.clients</groupId>
  13. <artifactId>jedis</artifactId>
  14. <version>2.9.0</version>
  15. </dependency>

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

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

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

  1. @Configuration
  2. @EnableCaching
  3. public class RedisConfig {
  4. @Bean
  5. public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
  6. RedisTemplate<String, Object> template = new RedisTemplate<>();
  7. template.setConnectionFactory(factory);
  8. // 使用Jackson2JsonRedisSerialize 替换默认的jdkSerializeable序列化
  9. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  10. ObjectMapper om = new ObjectMapper();
  11. om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  12. om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
  13. jackson2JsonRedisSerializer.setObjectMapper(om);
  14. StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
  15. // key采用String的序列化方式
  16. template.setKeySerializer(stringRedisSerializer);
  17. // hash的key也采用String的序列化方式
  18. template.setHashKeySerializer(stringRedisSerializer);
  19. // value序列化方式采用jackson
  20. template.setValueSerializer(jackson2JsonRedisSerializer);
  21. // hash的value序列化方式采用jackson
  22. template.setHashValueSerializer(jackson2JsonRedisSerializer);
  23. template.afterPropertiesSet();
  24. return template;
  25. }
  26. }

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

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

  1. @Component
  2. @Slf4j
  3. public class RedisService {
  4. @Autowired
  5. private RedisTemplate<String, Object> redisTemplate;
  6. /**
  7. * 指定缓存失效时间
  8. *
  9. * @param key 键
  10. * @param time 时间(秒)
  11. * @return
  12. */
  13. public boolean expire(String key, long time) {
  14. try {
  15. if (time > 0) {
  16. redisTemplate.expire(key, time, TimeUnit.SECONDS);
  17. }
  18. return true;
  19. } catch (Exception e) {
  20. log.error("exception when expire key {}. ", key, e);
  21. return false;
  22. }
  23. }
  24. /**
  25. * 根据key获取过期时间
  26. *
  27. * @param key 键 不能为null
  28. * @return 时间(秒) 返回0代表为永久有效
  29. */
  30. public long getExpire(String key) {
  31. return redisTemplate.getExpire(key, TimeUnit.SECONDS);
  32. }
  33. /**
  34. * 判断key是否存在
  35. *
  36. * @param key 键
  37. * @return true 存在 false不存在
  38. */
  39. public boolean hasKey(String key) {
  40. try {
  41. return redisTemplate.hasKey(key);
  42. } catch (Exception e) {
  43. log.error("exception when check key {}. ", key, e);
  44. return false;
  45. }
  46. }
  47. ...
  48. }

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

或在Service层使用注解,如

  1. @Service
  2. @CacheConfig(cacheNames = "users")
  3. public class UserService {
  4. private static Map<String, User> userMap = new HashMap<>();
  5. @CachePut(key = "#user.username")
  6. public User addUser(User user){
  7. user.setUid(UUID.randomUUID().toString());
  8. System.out.println("add user: " + user);
  9. userMap.put(user.getUsername(), user);
  10. return user;
  11. }
  12. @Caching(put = {
  13. @CachePut( key = "#user.username"),
  14. @CachePut( key = "#user.uid")
  15. })
  16. public User addUser2(User user) {
  17. user.setUid(UUID.randomUUID().toString());
  18. System.out.println("add user2: " + user);
  19. userMap.put(user.getUsername(), user);
  20. return user;
  21. }
  22. ...
  23. }

Spring Boot 2 整合Redis哨兵模式

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

  1. spring:
  2. redis:
  3. password: passw0rd
  4. timeout: 5000
  5. sentinel:
  6. master: mymaster
  7. nodes: 192.168.40.201:26379,192.168.40.201:36379,192.168.40.201:46379 # 哨兵的IP:Port列表
  8. jedis: # 或lettuce
  9. pool:
  10. max-active: 8
  11. max-wait: -1
  12. max-idle: 8
  13. min-idle: 0

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

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

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

  1. public JedisSentinelPool(String masterName, Set<String> sentinels,
  2. final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
  3. final String password, final int database, final String clientName) {
  4. this.poolConfig = poolConfig;
  5. this.connectionTimeout = connectionTimeout;
  6. this.soTimeout = soTimeout;
  7. this.password = password;
  8. this.database = database;
  9. this.clientName = clientName;
  10. HostAndPort master = initSentinels(sentinels, masterName);
  11. initPool(master);
  12. }
  13. private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
  14. for (String sentinel : sentinels) {
  15. final HostAndPort hap = HostAndPort.parseString(sentinel);
  16. log.fine("Connecting to Sentinel " + hap);
  17. Jedis jedis = null;
  18. try {
  19. jedis = new Jedis(hap.getHost(), hap.getPort());
  20. List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
  21. // connected to sentinel...
  22. sentinelAvailable = true;
  23. if (masterAddr == null || masterAddr.size() != 2) {
  24. log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
  25. + ".");
  26. continue;
  27. }
  28. master = toHostAndPort(masterAddr);
  29. log.fine("Found Redis master at " + master);
  30. break;
  31. } catch (JedisException e) {
  32. // resolves #1036, it should handle JedisException there's another chance
  33. // of raising JedisDataException
  34. log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e
  35. + ". Trying next one.");
  36. } finally {
  37. if (jedis != null) {
  38. jedis.close();
  39. }
  40. }
  41. }
  42. //省略了非关键代码
  43. for (String sentinel : sentinels) {
  44. final HostAndPort hap = HostAndPort.parseString(sentinel);
  45. MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
  46. // whether MasterListener threads are alive or not, process can be stopped
  47. masterListener.setDaemon(true);
  48. masterListeners.add(masterListener);
  49. masterListener.start();
  50. }
  51. return master;
  52. }

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

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

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

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

  1. private void initPool(HostAndPort master) {
  2. if (!master.equals(currentHostMaster)) {
  3. currentHostMaster = master;
  4. if (factory == null) {
  5. factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
  6. soTimeout, password, database, clientName, false, null, null, null);
  7. initPool(poolConfig, factory);
  8. } else {
  9. factory.setHostAndPort(currentHostMaster);
  10. // although we clear the pool, we still have to check the
  11. // returned object
  12. // in getResource, this call only clears idle instances, not
  13. // borrowed instances
  14. internalPool.clear();
  15. }
  16. log.info("Created JedisPool to master at " + master);
  17. }
  18. }

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

Spring Boot 2 整合Redis Cluster模式

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

  1. spring:
  2. redis:
  3. password: passw0rd
  4. timeout: 5000
  5. database: 0
  6. cluster:
  7. 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
  8. max-redirects: 3 # 重定向的最大次数
  9. jedis:
  10. pool:
  11. max-active: 8
  12. max-wait: -1
  13. max-idle: 8
  14. min-idle: 0

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

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

查看JedisClusterConnection的execute方法,

  1. public Object execute(String command, byte[]... args) {
  2. Assert.notNull(command, "Command must not be null!");
  3. Assert.notNull(args, "Args must not be null!");
  4. return clusterCommandExecutor
  5. .executeCommandOnArbitraryNode((JedisClusterCommandCallback<Object>) client -> JedisClientUtils.execute(command,
  6. EMPTY_2D_BYTE_ARRAY, args, () -> client))
  7. .getValue();
  8. }

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

  1. public <T> NodeResult<T> executeCommandOnArbitraryNode(ClusterCommandCallback<?, T> cmd) {
  2. Assert.notNull(cmd, "ClusterCommandCallback must not be null!");
  3. List<RedisClusterNode> nodes = new ArrayList<>(getClusterTopology().getActiveNodes());
  4. return executeCommandOnSingleNode(cmd, nodes.get(new Random().nextInt(nodes.size())));
  5. }
  6. private <S, T> NodeResult<T> executeCommandOnSingleNode(ClusterCommandCallback<S, T> cmd, RedisClusterNode node,
  7. int redirectCount) {
  8. Assert.notNull(cmd, "ClusterCommandCallback must not be null!");
  9. Assert.notNull(node, "RedisClusterNode must not be null!");
  10. if (redirectCount > maxRedirects) {
  11. throw new TooManyClusterRedirectionsException(String.format(
  12. "Cannot follow Cluster Redirects over more than %s legs. Please consider increasing the number of redirects to follow. Current value is: %s.",
  13. redirectCount, maxRedirects));
  14. }
  15. RedisClusterNode nodeToUse = lookupNode(node);
  16. S client = this.resourceProvider.getResourceForSpecificNode(nodeToUse);
  17. Assert.notNull(client, "Could not acquire resource for node. Is your cluster info up to date?");
  18. try {
  19. return new NodeResult<>(node, cmd.doInCluster(client));
  20. } catch (RuntimeException ex) {
  21. RuntimeException translatedException = convertToDataAccessException(ex);
  22. if (translatedException instanceof ClusterRedirectException) {
  23. ClusterRedirectException cre = (ClusterRedirectException) translatedException;
  24. return executeCommandOnSingleNode(cmd,
  25. topologyProvider.getTopology().lookup(cre.getTargetHost(), cre.getTargetPort()), redirectCount + 1);
  26. } else {
  27. throw translatedException != null ? translatedException : ex;
  28. }
  29. } finally {
  30. this.resourceProvider.returnResourceForSpecificNode(nodeToUse, client);
  31. }
  32. }

上述代码逻辑如下

  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. xpath_note - Ethan Lee

    https://ethan2lee.github.io/ XPath概览 XPath,全称XML Path Language,即XML路径语言,它是一门在XML文档中查找信息的语言.它最初是用来搜寻X ...

  2. AF(操作者框架)系列(3)-创建第一个Actor的程序

    这节课的内容,语言描述基本是无趣的,就是一个纯程序编写,直接上图了. 如果想做其他练习,可参考前面的文章: https://zhuanlan.zhihu.com/p/105133597 1. 新建一个 ...

  3. Python开发(二):列表、字典、元组与文件处理

    Python开发(二):列表.字典.元组与文件处理 一:列表二:元组三:字典四:文件处理 一:列表   为什么需要列表 可以通过列表可以对数据实现最方便的存储.修改等操作.字符串是不能修改的,所以无法 ...

  4. Mysql报错:Authentication .....Reading from the stream has failed

    连接Mysql5.7版本的数据库出现报错:Authentication to host '171.13.164.***' for user 'root' using method 'mysql_nat ...

  5. Python中max()内置函数使用(list)

    在学习完列表和元组的基础知识后,做到一个题: 求出列表中频次出现最多的元素. 学习到了python内置函数max的用法 其参数key的用法 匿名函数lamda的用法 python内置函数max() m ...

  6. 2020年春招面试必备Spring系列面试题129道(附答案解析)

    前言 关于Spring的知识总结了个思维导图分享给大家   1.不同版本的 Spring Framework 有哪些主要功能?   2.什么是 Spring Framework? Spring 是一个 ...

  7. vuex源码阅读分析

    这几天忙啊,有绝地求生要上分,英雄联盟新赛季需要上分,就懒着什么也没写,很惭愧.这个vuex,vue-router,vue的源码我半个月前就看的差不多了,但是懒,哈哈.下面是vuex的源码分析在分析源 ...

  8. spring——AOP原理及源码(五)

    前情回顾: 在上一篇中,通过 wrapIfNecessary 方法,我们获取到了合适的增强器(日志方法)与业务类进行包装,最终返回了我们业务类的代理对象. 本篇我们将从业务方法的执行开始,看看增强器( ...

  9. springboot1.5.9整合websocket实现实时显示的小demo

    最近由于项目需要实时显示数据库更新的数据变化情况,一开始想过在前端使用ajax异步轮询方法实现,但后面考虑到性能和流量等要求,就放弃该方法而选择使用websocket(毕竟现在springboot整合 ...

  10. Asp.Net Core 中IdentityServer4 授权中心之应用实战

    一.前言 查阅了大多数相关资料,查阅到的IdentityServer4 的相关文章大多是比较简单并且多是翻译官网的文档编写的,我这里在 Asp.Net Core 中IdentityServer4 的应 ...