springboot使用redisTemplate访问redis cluster(三主三从),底层是Lettuce,当其中一个master挂掉后,slave正常升为master,程序报错 Redis commond timed out after 6 seconds。
手动连接集群,正常读写,确定为应用程序的问题
查看应用程序的redis 集群配置,没有问题
查看网上的解决办法,发现是Lettuce的问题
转载:验证了方案二,把lettuce换成jedis,切换正常
最新一次线上生产环境下Redis集群服务器某一个主节点发生故障,Cluster节点下的从节点快速进行迁移升级为主节点,节点迁移时间大概为15秒,这15秒期间Redis服务不可用,程序无法读写Redis数据,报错java.lang.RuntimeException: org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s),但是15秒过后服务依旧无法使用,大概持续了6分钟,而在业务高峰期间这6分钟也会造成很大的用户感知,为何要持续这么久Redis才能恢复,成为了未知的谜团!
联合运维和云厂商做了很多测试,发现凡是使用jedis客户端的服务都可以在15秒主从切换后恢复,而使用lettuce作为redis客户端的服务则无法恢复使用,一直抛超时的异常,做了实验发现,使用lettuce作为客户端的服务,在15秒主从切换后一直要等待redis服务的宕机节点拉起成功后才可以恢复,而这时间大概持续了2分钟,从网上搜了很多答案发现也有一些遇到了同样问题的情况发生。Lettuce的节点切换15秒是来源于 cluster-node-timeout这个配置的默认时间,这个是时间节点宕机发现时间,也就是Redis群集节点不可用的最长时间,因为RedisCluster是无中心设计,节点探测的时间设置太小会因为网络抖动造成的节点下线,时间太长又无法快速处理节点切换,这个可以具体了解Cluster集群主从切换的原理。相关阅读https://www.cnblogs.com/kaleidoscope/p/9636264.html
因为所有微服务使用SpringBoot2.1.7版本SpringBoot2.X版本开始Redis默认的连接池都是采用的Lettuce,之前的文章也有介绍过Lettuce连接池的使用,为了避免后续出现硬件故障,导致服务连接Redis一段时间不可用的情况,所以也就急需要解决节点宕机的恢复时间问题。
经过大量的调研和实验最后发现有关,官方的描述是https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#user-content-refreshing-the-cluster-topology-view, Lettuce需要刷新节点拓扑视图,
大致意思是,Redis集群配置在运行期间可能会改变,可以添加新的节点,为特定插槽的主节点可以发生改变,Lettuce处理Moved和Ask永久重定向,但是由于命令重定向,你必须刷新节点拓扑视图,拓扑是绑定到RedisClusterClient的示例,所有由一个RedisClusterClient实例创建的节点连接共享相同的节点拓扑视图,视图可以采用以下三种方式更新
1、Either by calling RedisClusterClient.reloadPartitions
通过调用RedisClusterClient.reloadPartitions
2、Periodic updates in the background based on an interval
后台基于时间间隔的周期刷新
3、Adaptive updates in the background based on persistent disconnects and MOVED/ASKredirections
后台基于持续的断开和移动/重定向的自适应更新
By default, commands follow -ASK and -MOVED redirects up to 5 times until the command execution is considered to be failed. Background topology updating starts with the first connection obtained through RedisClusterClient.
默认的 命令跟随ASK 和移MOVED 命令执行重定向到5次,直到被认为是失败了,后台拓扑更新始于第一次RedisClusterClient链接
相关阅读 https://github.com/lettuce-io/lettuce-core/wiki/Client-options#periodic-cluster-topology-refresh
所以说在RedisCluster集群模式下可以通过 3种方式去刷新节点拓扑视图去解决节点重新识别的问题,
第一种方式是通过RedisClusterClient,SpringBoot通过Sprint Redis Data构建Redis时,没有显式构建RedisClusterClient,所以只能通过其他两种方式
https://github.com/lettuce-io/lettuce-core/wiki/Client-Options
这里描述了很多特殊场景下设置的客户端选项,可以视自身情况去设置调整
@Autowired private RedisProperties redisProperties; @Bean public GenericObjectPoolConfig> genericObjectPoolConfig(Pool properties) { GenericObjectPoolConfig> config = new GenericObjectPoolConfig<>(); config.setMaxTotal(properties.getMaxActive()); config.setMaxIdle(properties.getMaxIdle()); config.setMinIdle(properties.getMinIdle()); if (properties.getTimeBetweenEvictionRuns() !=null) { config.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRuns().toMillis()); } if (properties.getMaxWait() !=null) { config.setMaxWaitMillis(properties.getMaxWait().toMillis()); } return config; } @Bean(destroyMethod ="destroy") public LettuceConnectionFactory lettuceConnectionFactory() { //开启 自适应集群拓扑刷新和周期拓扑刷新 ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder() // 开启全部自适应刷新 .enableAllAdaptiveRefreshTriggers() // 开启自适应刷新,自适应刷新不开启,Redis集群变更时将会导致连接异常 // 自适应刷新超时时间(默认30秒) .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30)) //默认关闭开启后时间为30秒 // 开周期刷新 .enablePeriodicRefresh(Duration.ofSeconds(20)) // 默认关闭开启后时间为60秒 ClusterTopologyRefreshOptions.DEFAULT_REFRESH_PERIOD 60 .enablePeriodicRefresh(Duration.ofSeconds(2)) = .enablePeriodicRefresh().refreshPeriod(Duration.ofSeconds(2)) .build(); // https://github.com/lettuce-io/lettuce-core/wiki/Client-Options ClientOptions clientOptions = ClusterClientOptions.builder() .topologyRefreshOptions(clusterTopologyRefreshOptions) .build(); LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() .poolConfig(genericObjectPoolConfig(redisProperties.getLettuce().getPool())) //.readFrom(ReadFrom.MASTER_PREFERRED) .clientOptions(clientOptions) .commandTimeout(redisProperties.getTimeout()) //默认RedisURI.DEFAULT_TIMEOUT 60 .build(); List clusterNodes = redisProperties.getCluster().getNodes(); Set nodes = new HashSet(); clusterNodes.forEach(address -> nodes.add(new RedisNode(address.split(":")[0].trim(), Integer.valueOf(address.split(":")[1])))); RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration(); clusterConfiguration.setClusterNodes(nodes); clusterConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword())); clusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects()); LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(clusterConfiguration, clientConfig); // lettuceConnectionFactory.setShareNativeConnection(false); //是否允许多个线程操作共用同一个缓存连接,默认true,false时每个操作都将开辟新的连接 // lettuceConnectionFactory.resetConnection(); // 重置底层共享连接, 在接下来的访问时初始化 return lettuceConnectionFactory; }
开启自适应刷新并设定刷新频率
可以看到设定前,周期刷新和拓扑刷新都是false
调整后周期刷新和拓扑刷新都是true
enablePeriodicRefresh意思就是开启并设定周期刷新时间
开关的开启后的控制实际是RedisClusterClient.activateTopologyRefreshIfNeeded在这个方法内完成的,如果开关开启则会创建一个ScheduledFuture 根据你设置的节点刷新事件定期的去调用,当RedisClusterClient初始化后,定时器会周期性的执行,
如果 定时器执行通过,则RedisClusterClient.doLoadPartitions会返回loadedPartitions,如果半截Return掉,则不再返回新的节点信息。
相关阅读https://github.com/lettuce-io/lettuce-core/issues/240
相关阅读https://blog.csdn.net/weixin_42182797/article/details/95210437#_1
当然,如果你想就此放弃lettuce转用jedis也是可以的 Spring Boot2.X版本,只要在pom.xml里,调整一下依赖包的引用
org.springframework.boot spring-boot-starter-data-redis io.lettuce lettuce-core redis.clients jedis
配置上lettuce换成jedis的,既可以完成底层对jedis的替换
spring: redis: database: 0 #Redis 索引(0~15,默认为0) timeout: 1000 #Redis 连接的超时时间 password: #Redis 密码,如果没有就默认不配置此参数 cluster: #Redis 集群配置 max-redirects: 5 #Redis 命令执行时最多转发次数 nodes: 192.168.56.15:7000,192.168.56.15:7001,192.168.56.16:7002,192.168.56.16:7003,192.168.56.17:7004,192.168.56.17:7005 #Redis 集群地址 jedis: pool: max-active: 20 max-wait: -1 min-idle: 0 max-idle: 10#使用 lettuce 连接池# lettuce: # pool:# max-active: 20 #连接池最大连接数(使用负值表示没有限制)# max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制)# min-idle: 0 #连接池中的最大空闲连接# max-idle: 10 #连接池中的最小空闲连接
因为jedis的节点信息,没有搞的那么复杂