起因
最近專案上發現一個問題,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
當然,如果你想就此放棄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和Lettuce區別
Lettuce 和 Jedis 的定位都是Redis的client,所以他們當然可以直接連線redis server。
Jedis在實現上是直接連線的redis server,如果在多執行緒環境下是非執行緒安全的,這個時候只有使用連線池,為每個Jedis例項增加物理連線
Lettuce的連線是基於Netty的,連線例項(StatefulRedisConnection)可以在多個執行緒間併發訪問,應為StatefulRedisConnection是執行緒安全的,所以一個連線例項(StatefulRedisConnection)就可以滿足多執行緒環境下的併發訪問,當然這個也是可伸縮的設計,一個連線例項不夠的情況也可以按需增加連線例項。