最新一次線上生產環境下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才能恢復,成爲了未知的謎團!html
聯合運維和雲廠商作了不少測試,發現凡是使用jedis客戶端的服務均可以在15秒主從切換後恢復,而使用lettuce做爲redis客戶端的服務則沒法恢復使用,一直拋超時的異常,作了實驗發現,使用lettuce做爲客戶端的服務,在15秒主從切換後一直要等待redis服務的宕機節點拉起成功後才能夠恢復,而這時間大概持續了2分鐘,從網上搜了不少答案發現也有一些遇到了一樣問題的狀況發生。Lettuce的節點切換15秒是來源於 cluster-node-timeout這個配置的默認時間,這個是時間節點宕機發現時間,也就是Redis羣集節點不可用的最長時間,由於RedisCluster是無中心設計,節點探測的時間設置過小會由於網絡抖動形成的節點下線,時間太長又沒法快速處理節點切換,這個能夠具體瞭解Cluster集羣主從切換的原理。相關閱讀https://www.cnblogs.com/kaleidoscope/p/9636264.htmljava
由於全部微服務使用SpringBoot2.1.7版本SpringBoot2.X版本開始Redis默認的鏈接池都是採用的Lettuce,以前的文章也有介紹過Lettuce鏈接池的使用,爲了不後續出現硬件故障,致使服務鏈接Redis一段時間不可用的狀況,因此也就急須要解決節點宕機的恢復時間問題。node
通過大量的調研和實驗最後發現有關,官方的描述是https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#user-content-refreshing-the-cluster-topology-view, Lettuce須要刷新節點拓撲視圖,git
大體意思是,Redis集羣配置在運行期間可能會改變,能夠添加新的節點,爲特定插槽的主節點能夠發生改變,Lettuce處理Moved和Ask永久重定向,可是因爲命令重定向,你必須刷新節點拓撲視圖,拓撲是綁定到RedisClusterClient的示例,全部由一個RedisClusterClient實例建立的節點鏈接共享相同的節點拓撲視圖,視圖能夠採用如下三種方式更新github
一、Either by calling RedisClusterClient.reloadPartitionsweb
經過調用RedisClusterClient.reloadPartitionsredis
二、Periodic updates in the background based on an intervalspring
後臺基於時間間隔的週期刷新緩存
三、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<String> clusterNodes = redisProperties.getCluster().getNodes(); Set<RedisNode> nodes = new HashSet<RedisNode>(); 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裏,調整一下依賴包的引用
<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> </dependency>
配置上lettuce換成jedis的,既能夠完成底層對jedis的替換
spring: redis: jedis: pool: max-active: ${redis.config.maxTotal:1024} max-idle: ${redis.config.maxIdle:50} min-idle: ${redis.config.minIdle:1} max-wait: ${redis.config.maxWaitMillis:5000} #lettuce: #pool: #max-active: ${redis.config.maxTotal:1024} #max-idle: ${redis.config.maxIdle:50} #min-idle: ${redis.config.minIdle:1} #max-wait: ${redis.config.maxWaitMillis:5000}
由於jedis的節點信息,沒有搞的那麼複雜