Spring Boot 2.3.0 Redis拓扑动态感应,使用Lettuce拓扑刷新

背景

关于 Redis 在生产中我们一般情况下都会选择 redis cluster 高可用架构部署,既能保证数据分片并且实现节点的故障自动转移。 基本部署拓扑如下:

配置项含义数据类型默认值示例
spring.redis.databaseRedis中可以有多个db,设置数据库索引int00
spring.redis.urlRedis连接串Stringredis://user:password@example.com:6379
spring.redis.hostRedis主机Stringlocalhost172.16.100.65
spring.redis.portRedis端口int63796379
spring.redis.password密码String123456
spring.redis.ssl是否开启SSL支持booleanfalsetrue
spring.redis.timeout连接超时,自己指定单位Duration30s30000ms
spring.redis.sentinel.master哨兵模式下,Redis主服务器地址Stringmymaster
spring.redis.sentinel.password哨兵模式的认证密码String123456
spring.redis.sentinel.nodes哨兵模式下,逗号分隔的键值对(主机:端口)形式的服务器列表List<String>192.168.110.2:6379,192.168.110.3:6380
spring.redis.cluster.maxRedirects集群模式下,集群最大转发的数量Integer3
spring.redis.cluster.nodes集群模式下,逗号分隔的键值对(主机:端口)形式的服务器列表List<String>192.168.110.2:6379,192.168.110.3:6380
spring.redis.lettuce.shutdownTimeout关机超时时限Duration100ms1s
spring.redis.lettuce.cluster.refresh.period集群拓扑刷新周期Duration300s
spring.redis.lettuce.cluster.refresh.adaptivebooleanfalsefalse
spring.redis.lettuce.pool.maxIdle连接池中的最大空闲连接数int8100
spring.redis.lettuce.pool.minIdle连接池中的最小空闲连接数int0100
spring.redis.lettuce.pool.maxActive连接池最大连接数(使用负值表示没有限制)int8100
spring.redis.lettuce.pool.maxWait连接池最大阻塞等待时间,(使用负值表示没有限制)Duration-110s
spring.redis.lettuce.pool.timeBetweenEvictionRuns空闲连接回收间隔,为负时不创建空闲回收线程,为正时表示定时空闲连接回收间隔Duration60s

创建测试集群

  • 这里通过我封装的 pig4cloud/redis-cluster:4.0 镜像,即可构建一个 6 个节点的 redis cluster 测试环境。
docker run --name redis-cluster -d -e CLUSTER_ANNOUNCE_IP=宿主机IP \
-p 7000-7005:7000-7005 -p 17000-17005:17000-17005  pig4cloud/redis-cluster:4.0
复制代码
  • 查看集群节点信息
⋊> ./redis-cli -h 172.17.0.111 -p 7000 -c                     16:09:48
172.17.0.111:7000> cluster nodes
3d882206d40935beef84ff564b538d57369e4fd9 172.17.0.111:7003@17003 slave b8d24150df4a221c1045cd9a0696bd1972912d52 0 1591344590000 4 connected
b8d24150df4a221c1045cd9a0696bd1972912d52 172.17.0.111:7001@17001 master - 0 1591344590513 2 connected 5461-10922
c21167a6da7f8af31d2dd612d449cdf92ad2e7e9 172.17.0.111:7005@17005 slave 810baa140db6e008a137708f09d4335f5207ede3 0 1591344591000 6 connected
810baa140db6e008a137708f09d4335f5207ede3 172.17.0.111:7000@17000 myself,master - 0 1591344590000 1 connected 0-5460
05d2f9884d350a50ac9e38f575b57f19e864e74c 172.17.0.111:7004@17004 slave b3cf24a918d96a1949f49a1d7b3a965ff9dc858c 0 1591344590011 5 connected
b3cf24a918d96a1949f49a1d7b3a965ff9dc858c 172.17.0.111:7002@17002 master - 0 1591344591617 3 connected 10923-16383
复制代码

应用层接入集群

  • 这里使用 spring boot 2.2 演示, 默认的连接池使用 lettuce
spring:
  redis:
    cluster:
      nodes:
        - 172.17.0.111:7000
        - 172.17.0.111:7001
        - 172.17.0.111:7002
        - 172.17.0.111:7003
        - 172.17.0.111:7004
        - 172.17.0.111:7005
复制代码
  • 简单使用 redisTemplate 操作集群
@RestController
public class DemoController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("/add")
    public String redis() {
        redisTemplate.opsForValue().set("k1", "v1");
        return "ok";
    }
}
复制代码
  • 调用查看日志
⋊> curl http://localhost:8080/add
ok⏎
复制代码

我们会发现操作 k1 是在 7000 节点进行操作写入

[channel=0x5ff7aa8f, /172.17.0.156:50783 -> /172.17.0.111:7000, epid=0x8] write() writeAndFlush command ClusterCommand [command=AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command], redirections=0, maxRedirections=5]
[channel=0x5ff7aa8f, /172.17.0.156:50783 -> /172.17.0.111:7000, epid=0x8] write() done
复制代码

模拟单点故障

  • 关闭 7000 节点
./redis-cli -h 172.17.0.111 -p 7000 -c
172.17.0.111:7000> SHUTDOWN
复制代码
  • 查看 redis cluster 集群日志 docker logs -f redis-cluster

我们可以看到此时集群选举完毕,完成故障转移

23:S 05 Jun 08:24:49.387 # Starting a failover election for epoch 7.
29:M 05 Jun 08:24:49.388 # Failover auth granted to c21167a6da7f8af31d2dd612d449cdf92ad2e7e9 for epoch 7
26:M 05 Jun 08:24:49.388 # Failover auth granted to c21167a6da7f8af31d2dd612d449cdf92ad2e7e9 for epoch 7
23:S 05 Jun 08:24:49.389 # Failover election won: I'm the new master.
23:S 05 Jun 08:24:49.389 # configEpoch set to 7 after successful failover
23:M 05 Jun 08:24:49.389 # Setting secondary replication ID to 5253748ecf5bd7ab3536058fba8cad62d2d5e825, valid up to offset: 1622. New replication ID is 21d6a0b199a1ba655c0279d9c78f9682477ac9a3
23:M 05 Jun 08:24:49.389 * Discarding previously cached master state.
23:M 05 Jun 08:24:49.390 # Cluster state changed: ok
复制代码
  • 此时集群状态。7005 从 slave 变更为 master

应用层日志

  • 大量输出连接 7000 节点异常
io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: /172.17.0.111:7000
Caused by: java.net.ConnectException: Connection refused

io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: /172.17.0.111:7000
Caused by: java.net.ConnectException: Connection refused


io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: /172.17.0.111:7000
Caused by: java.net.ConnectException: Connection refused
复制代码
  • 再次操作 redisTemplate 会发现卡死,等待结果返回
⋊> curl http://localhost:8080/add
复制代码
  • 原因分析

此时还是操作 k1, 根据 slot 对应连接到 7000 节点,已经连接不到无限尝试重连的问题。 lettuce 客户端并未和 redis cluster 集群状态同步刷新,把宕机节点移除,完成故障转移。

集群拓扑动态感应

拓扑动态感应即客户端能够根据 redis cluster 集群的变化,动态改变客户端的节点情况,完成故障转移。

我们只需要在 spring boot 2.3.0 版本中 开启此特性即可。

spring.redis.lettuce.cluster.refresh.adaptive=true
spring.redis.lettuce.cluster.refresh.period=10000  #单位毫秒
spring:
  redis:
    lettuce:
      cluster:
        refresh:
          adaptive: true

其实 lettuce 官方一直有这个功能,但 spring data redis 并未跟进,具体内容可以参考 user-content-refreshing-the-cluster-topology-view 章节

其他解决方案

解决方案1:

  1. 升级到SpringBoot2.3.0或以上版本。并添加如下配置项
spring.redis.timeout=60s
spring.redis.lettuce.cluster.refresh.period=60s
spring.redis.lettuce.cluster.refresh.adaptive=true

解决方案2:
配置LettuceConnectionFactory ,设置拓扑刷新策略。

@Bean
public DefaultClientResources lettuceClientResources() {
    return DefaultClientResources.create();
}

@Bean
public LettuceConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties, ClientResources clientResources) {

    ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
            .enablePeriodicRefresh(Duration.ofSeconds(30)) //按照周期刷新拓扑
            .enableAllAdaptiveRefreshTriggers() //根据事件刷新拓扑
            .build();

    ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
            //redis命令超时时间,超时后才会使用新的拓扑信息重新建立连接
            .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(10)))
            .topologyRefreshOptions(topologyRefreshOptions)
            .build();

    LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
            .clientResources(clientResources)
            .clientOptions(clusterClientOptions)
            .build();

    RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
    clusterConfig.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
    clusterConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));

    LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(clusterConfig, clientConfiguration);

    return lettuceConnectionFactory;
}

解决方案3:

  1. 将spring-boot-starter-data-redis依赖的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>
</dependency>

旧版本兼容

我们只需要参考 adaptive 开关打开后做了哪些事情,给自己的项目配置上 topology-view 即可

例子1

	@Bean
	public LettuceConnectionFactory redisConnectionFactory(RedisProperties redisProperties) {
		RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());

		// https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#user-content-refreshing-the-cluster-topology-view
		ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
				.enablePeriodicRefresh()
				.enableAllAdaptiveRefreshTriggers()
				.refreshPeriod(Duration.ofSeconds(5))
				.build();

		ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
				.topologyRefreshOptions(clusterTopologyRefreshOptions).build();

		// https://github.com/lettuce-io/lettuce-core/wiki/ReadFrom-Settings
		LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder()
				.readFrom(ReadFrom.REPLICA_PREFERRED)
				.clientOptions(clusterClientOptions).build();

		return new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
	}

例子2

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;
    }

   开启自适应刷新并设定刷新频率
 

例子3

继续查找,发现了Lettuce官方给了开启拓扑刷新的代码例子

# Example 37. Enabling periodic cluster topology view updates
RedisClusterClient clusterClient = RedisClusterClient.create(RedisURI.create("localhost", 6379));

ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                                .enablePeriodicRefresh(10, TimeUnit.MINUTES)
                                .build();

clusterClient.setOptions(ClusterClientOptions.builder()
                                .topologyRefreshOptions(topologyRefreshOptions)
                                .build());
...

clusterClient.shutdown();


# Example 38. Enabling adaptive cluster topology view updates
RedisURI node1 = RedisURI.create("node1", 6379);
RedisURI node2 = RedisURI.create("node2", 6379);

RedisClusterClient clusterClient = RedisClusterClient.create(Arrays.asList(node1, node2));

ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                                .enableAdaptiveRefreshTrigger(RefreshTrigger.MOVED_REDIRECT, RefreshTrigger.PERSISTENT_RECONNECTS)
                                .adaptiveRefreshTriggersTimeout(30, TimeUnit.SECONDS)
                                .build();

clusterClient.setOptions(ClusterClientOptions.builder()
                                .topologyRefreshOptions(topologyRefreshOptions)
                                .build());
...

clusterClient.shutdown();
复制代码

根据示例我们修改一下我们的项目代码

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


import java.time.Duration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Configuration
public class LettuceRedisConfig {


    @Autowired
    private RedisProperties redisProperties;

    /**
     * 配置RedisTemplate
     * 【Redis配置最终一步】
     *
     * @param lettuceConnectionFactoryUvPv redis连接工厂实现
     * @return 返回一个可以使用的RedisTemplate实例
     */
    @Bean
    public RedisTemplate redisTemplate(@Qualifier("lettuceConnectionFactoryUvPv") RedisConnectionFactory lettuceConnectionFactoryUvPv) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(lettuceConnectionFactoryUvPv);
        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配置Redis连接工厂实现
     * LettuceConnectionFactory实现了RedisConnectionFactory接口
     * UVPV用Redis
     *
     * @return 返回LettuceConnectionFactory
     */
    @Bean(destroyMethod = "destroy")
    //这里要注意的是,在构建LettuceConnectionFactory 时,如果不使用内置的destroyMethod,可能会导致Redis连接早于其它Bean被销毁
    public LettuceConnectionFactory lettuceConnectionFactoryUvPv() throws Exception {

        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());

        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
        poolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
        poolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());

        return new LettuceConnectionFactory(clusterConfiguration, getLettuceClientConfiguration(poolConfig));
    }

    /**
     * 配置LettuceClientConfiguration 包括线程池配置和安全项配置
     *
     * @param genericObjectPoolConfig common-pool2线程池
     * @return lettuceClientConfiguration
     */
    private LettuceClientConfiguration getLettuceClientConfiguration(GenericObjectPoolConfig genericObjectPoolConfig) {
        /*
        ClusterTopologyRefreshOptions配置用于开启自适应刷新和定时刷新。如自适应刷新不开启,Redis集群变更时将会导致连接异常!
         */
        ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                //开启自适应刷新
                //.enableAdaptiveRefreshTrigger(ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT, ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS)
                //开启所有自适应刷新,MOVED,ASK,PERSISTENT都会触发
                .enableAllAdaptiveRefreshTriggers()
                // 自适应刷新超时时间(默认30秒)
                .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(25)) //默认关闭开启后时间为30秒
                // 开周期刷新
                .enablePeriodicRefresh(Duration.ofSeconds(20))  // 默认关闭开启后时间为60秒 ClusterTopologyRefreshOptions.DEFAULT_REFRESH_PERIOD 60  .enablePeriodicRefresh(Duration.ofSeconds(2)) = .enablePeriodicRefresh().refreshPeriod(Duration.ofSeconds(2))
                .build();
        return LettucePoolingClientConfiguration.builder()
                .poolConfig(genericObjectPoolConfig)
                .clientOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build())
                //将appID传入连接,方便Redis监控中查看
                //.clientName(appName + "_lettuce")
                .build();
    }
}
复制代码

之后在工具类中注入redisTemplate类即可

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;


参考:

Spring Boot 2.3.0 新特性Redis 拓扑动态感应 - 掘金

RedisCluster集群模式下master宕机主从切换期间Lettuce连接Redis无法使用报错Redis command timed out的问题_redis lecture_幕紫的博客-CSDN博客

https://juejin.im/post/5e12e39cf265da5d381d0f00

<think>嗯,用户遇到了Redis的READONLY错误,想用Spring Data RedisLettuce解决这个问题。首先,我需要回忆一下这个错误的原因。根据引用内容,这个错误是因为尝试往只读副本(replica)执行写操作导致的。Redis的主从架构中,从节点默认是只读的,用户可能在配置中错误地将应用指向了从节点,或者主节点故障后没有正确切换。 接下来,我需要考虑可能的解决方案。首先,检查Redis连接配置是否正确,确保写操作发送到主节点。如果使用了集群或哨兵模式,可能需要检查配置是否正确。然后,可以尝试在从节点设置`replica-read-only no`,不过默认是yes,修改这个可能会有风险,需要谨慎。另外,Lettuce客户端可能需要配置自动拓扑刷新,以便在主从切换时自动更新连接。 用户提到使用的是Spring Data RedisLettuce,所以需要具体到这些技术的配置。例如,检查Spring的配置文件中的spring.redis.url或cluster/nodes设置是否正确。Lettuce的配置可能需要开启定时刷新拓扑和自适应拓扑更新。 另外,异常处理方面,建议在代码中添加重试机制,比如使用Spring Retry,在捕获到RedisReadOnlyException时进行重试,或者降级处理。同时,监控和告警也是必要的,及时发现主从切换或配置错误的情况。 还要考虑版本兼容性问题,确保Spring Data RedisLettuce的版本支持自动拓扑刷新。如果用户的环境是哨兵模式,需要配置哨兵节点的信息,让客户端能够自动发现主节点。 总结下来,解决步骤应该是:1. 检查连接配置,确保写操作到主节点;2. 配置Lettuce自动刷新拓扑3. 必要时调整Redis副本的只读设置;4. 代码中添加异常处理机制;5. 设置监控告警。同时,引用中提到的错误信息需要对应到具体的解决方案,比如引用[1]和[2]提到的异常,可能需要检查配置和拓扑刷新。</think>### Redis主从架构下出现"READONLY"错误的解决方案 当使用Spring Data Redis连接Redis主从集群时,若出现`io.lettuce.core.RedisReadOnlyException: READONLY You can&#39;t write against a read only replica`错误,可通过以下步骤排查解决: --- #### 一、根本原因分析 1. **主从拓扑识别错误**:客户端错误地将写请求发送到从节点(Replica),而Redis从节点默认开启只读模式(`replica-read-only yes`)[^1][^2] 2. **主节点故障转移未完成**:主节点宕机后,集群尚未完成故障转移或客户端未及时感知新拓扑 3. **连接池配置错误**:连接池中的连接指向了错误的节点类型 --- #### 二、解决方案步骤 1. **验证Redis节点角色** ```bash # 连接到疑似主节点的Redis实例 redis-cli -h {host} -p {port} > INFO replication # 查看role字段确认角色(master/replica) ``` 2. **Spring Boot配置检查** ```properties # 确保连接主节点(单节点模式) spring.redis.host=主节点IP spring.redis.port=6379 # 哨兵模式正确配置 spring.redis.sentinel.master=mymaster spring.redis.sentinel.nodes=哨兵节点1:26379,哨兵节点2:26379 ``` 3. **Lettuce客户端配置优化** ```java @Configuration public class RedisConfig { @Bean public LettuceClientConfigurationBuilderCustomizer lettuceCustomizer() { return clientConfigurationBuilder -> { clientConfigurationBuilder .clientOptions(ClientOptions.builder() .autoReconnect(true) .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) .build()) .commandTimeout(Duration.ofSeconds(30)); }; } } ``` 4. **启用拓扑刷新(关键配置)** ```properties # 开启自动拓扑刷新Spring Boot 2.3+) spring.redis.lettuce.cluster.refresh.adaptive=true spring.redis.lettuce.cluster.refresh.period=2000 ``` 5. **应急处理方案** ```java @Retryable(value = RedisReadOnlyException.class, maxAttempts = 3, backoff = @Backoff(delay = 100)) public void writeOperation(String key, String value) { redisTemplate.opsForValue().set(key, value); } ``` --- #### 三、预防措施 1. **网络拓扑监控**:通过`RedisClusterClient#getPartitions()`实时获取集群分区信息 2. **连接验证机制**: ```java redisTemplate.execute((RedisCallback<Object>) connection -> { connection.ping(); return null; }); ``` 3. **版本兼容性检查**: - Lettuce ≥ 5.3.4.RELEASE - Spring Boot2.3.0.RELEASE --- #### 四、高级场景处理 对于**混合部署环境**(部分节点在K8s集群,部分在物理机),需要特别注意: ```properties # 强制指定首选地址类型 spring.redis.lettuce.cluster.preferred-addr-type=PUBLIC ``` ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值