最近项目上发现一个问题,redis cluster集群有一台机崩了之后,后台服务的redis会一直报错,无法连接到redis集群。通过命令查看redis集群,发现redis cluster集群是正常的,备用的slave机器已经升级为master。
于是初步猜测是spring-redis的连接池框架在redis的其中一台master机器崩了之后,并没有刷新连接池的连接,仍然连接的是挂掉的那台redis服务器。
通过寻找资料,发现springboot在1.x使用的是jedis框架,在2.x改为默认使用Lettuce框架与redis连接。 在Lettuce官方文档中找到了关于Redis Cluster的相关信息 《Refreshing the cluster topology view》
这里面的大概意思是 自适应拓扑刷新(Adaptive updates)与定时拓扑刷新(Periodic updates) 是默认关闭的,可以通过代码打开。
继续查找,发现了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; 复制代码
我们可以通过debug断点查看redisTemplate类的参数来观察拓扑刷新是否开启,在未设置之前,我们的redisTemplate的周期刷新和拓扑刷新都是false
在设置了开启拓扑刷新之后,可以看到我们的两个参数值都变成了true
其中clusterTopologyRefreshActivated参数并不是根据我们增加的拓扑刷新开启就初始化为true的,它是在第一次使用redisTemplate的时候,在 RedisClusterClient#activateTopologyRefreshIfNeeded
方法中修改为true的,有兴趣的可以自行debug跟踪一下。
当然,如果你想就此放弃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} 复制代码
Lettuce相较于Jedis有哪些优缺点?
Lettuce 和 Jedis 的定位都是Redis的client,所以他们当然可以直接连接redis server。
Jedis在实现上是直接连接的redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个Jedis实例增加物理连接
Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。