深圳幻海软件技术有限公司 欢迎您!

Redis Cluster集群,当Master宕机,主从切换,客户端报错 Timed Out

2023-02-28

大家好,我是Tom哥。性能不够,缓存来凑。一个高并发系统肯定少不了缓存的身影,为了保证缓存服务的高可用,我们通常采用RedisCluster集群模式。描述:集群部署采用了3主3从拓扑结构,数据读写访问master节点,slave节点负责备份。随便登录一台redis节点,都可以看到集群的slot的槽位

大家好,我是Tom哥。

性能不够,缓存来凑。

一个高并发系统肯定少不了缓存的身影,为了保证缓存服务的高可用,我们通常采用 Redis Cluster 集群模式。

描述:

集群部署采用了 3主3从 拓扑结构,数据读写访问master节点, slave节点负责备份。

随便登录一台 redis 节点,都可以看到集群的slot的槽位分步区间,以及对应的主从节点映射关系。

127.0.0.1:8001> cluster slots
1) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 8003
      3) "6c574c9d1323c69ebc73a5977bcbd3d4c073a4d4"
   4) 1) "127.0.0.1"
      2) (integer) 8006
      3) "123d0b157078925743ac1deb96be8c3395d7d038"
2) 1) (integer) 0
   2) (integer) 5460
   3) 1) "127.0.0.1"
      2) (integer) 8001
      3) "99bc05e81ef0035a4ab2d13cbae2599425b7ed7d"
   4) 1) "127.0.0.1"
      2) (integer) 8004
      3) "402e900ef364ce9382beddf92747cf28e3ea9c2f"
3) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "127.0.0.1"
      2) (integer) 8002
      3) "fda6a9e49205a52418c0bca4c66c981066017a3c"
   4) 1) "127.0.0.1"
      2) (integer) 8005
      3) "24a1e23f6cbfb761234970b66043d562e79e3d9c"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

人为模拟,master-1 机器意外宕机。

docker stop c1dff012392d
  • 1.

此时,Redis Cluster 集群能自动感知,并自动完成主备切换,对应的slave会被选举为新的master节点。

看下 redis cluster 集群最新的主从关系。

看似也没什么问题,一切正常。

此时 Spring Boot 应用依然在线服务,当我们再尝试操作缓存时,会报错。

问题边界还是非常清晰的。

Redis Cluster 集群已经完成了切换。

但是 Spring Boot 客户端没有动态感知到 Redis Cluster 的最新集群信息

原因分析:

SpringBoot 2.X 版本, Redis默认的连接池采用 Lettuce。

当Redis 集群节点发生变化后,Letture默认是不会刷新节点拓扑。

解决方案:

将 Letture 二方包仲裁掉。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.3.12.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

然后,引入 Jedis 相关二方包。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

编译代码,并重新启动 SpringBoot 微服务,万事俱备,只欠再次验证。

重新模拟将 127.0.0.1:8001 master 节点宕机,看看系统的日志。

[2022-03-17 18:03:34:595] - master /127.0.0.1:8001 used as slave
[2022-03-17 18:03:34:596] - slave redis://127.0.0.1:8004 removed for slot ranges: [[0-5460]]
[2022-03-17 18:03:34:611] - 1 connections initialized for /127.0.0.1:8004
[2022-03-17 18:03:34:639] - /127.0.0.1:8001 master and related slaves: [addr=redis://127.0.0.1:8004] removed
[2022-03-17 18:03:34:641] - 24 connections initialized for /127.0.0.1:8004
[2022-03-17 18:03:34:655] - 1 connections initialized for /127.0.0.1:8004
[2022-03-17 18:03:34:678] - master: redis://127.0.0.1:8004 added for slot ranges: [[0-5460]]
[2022-03-17 18:03:34:678] - 24 connections initialized for /127.0.0.1:8004
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

从打印的日志来看,客户端已经感知到了主备切换,并与最新的主节点 127.0.0.1:8004 初始化了 24 个连接。

然后,回归业务功能,读写缓存 数据也都是操作最新的主节点。

还有一种方案:刷新节点拓扑视图。

Lettuce 官方描述:

https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#user-content-refreshing-the-cluster-topology-view。

Lettuce 处理 Moved 和 Ask 永久重定向,由于命令重定向,必须刷新节点拓扑视图。而自适应拓扑刷新(Adaptive updates)与定时拓扑刷新(Periodic updates)默认关闭。

解决方案:

  • 调用 RedisClusterClient.reloadPartitions。
  • 后台基于时间间隔的周期刷新。
  • 后台基于持续的断开 和 移动、重定向 的自适应更新。

编写代码

@Bean(destroyMethod = "destroy")
public LettuceConnectionFactory lettuceConnectionFactory() {
    // 
    ClusterTopologyRefreshOptions clusterTopologyRefreshOptions =  ClusterTopologyRefreshOptions.builder()
            // ,Redis
            .enableAllAdaptiveRefreshTriggers() 
            // (认30)
            .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30)) 
            // 
            .enablePeriodicRefresh(Duration.ofSeconds(20))  
            .build();
    ClientOptions clientOptions = ClusterClientOptions.builder()
            .topologyRefreshOptions(clusterTopologyRefreshOptions)
            .build();
    LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
            .poolConfig(genericObjectPoolConfig(redisProperties.getJedis().getPool()))
            .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);
    // 线认true,false 
    // lettuceConnectionFactory.setShareNativeConnection(false); 
    // , 访
    // lettuceConnectionFactory.resetConnection(); 
    return lettuceConnectionFactory;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.