推荐:2020金三银四Java岗面试必备:基础+框架+并发+Netty+缓存+Dubbo+MySQL+ZK+Linux...

改变规则、让人兴奋的Spring Boot,这份实战文档会让你弦无虚发

起因

最近项目上发现一个问题,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官方给了开启拓扑刷新的代码例子

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

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;@Configurationpublic 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

当然,如果你想就此放弃Lettuce转用jedis也是可以的。在Spring Boot2.X版本,只要在pom.xml里,调整一下依赖包的引用即可:

配置上lettuce换成jedis的,既可以完成底层对jedis的替换

Jedis和Lettuce区别

Lettuce相较于Jedis有哪些优缺点?

Lettuce 和 Jedis 的定位都是Redis的client,所以他们当然可以直接连接redis server。

Jedis在实现上是直接连接的redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个Jedis实例增加物理连接

Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。