在一個Spring boot項目中,需要使用redis作為緩存,于是將使用spring-boot-starter-data-redis,具體依賴如下:
org.springframework.boot spring-boot-starter-data-redis 2.0.4.RELEASE
在測試環境中,功能測試,壓力測試,都沒有發現問題,原因是測試環境中redis自行搭建,沒有設置密碼,但是上線后,Redis使用的是A***的Pass服務的集群,并設置密碼,使用過程中發現如下問題:
(相關資料圖)
redis負載高;redis異常,錯誤信息:com.lambdaworks.redis.RedisException: java.lang.IllegalArgumentException: Connection to XXX.XX.XXX.XXX:15000 not allowed. This connection point is not known in the cluster viewjava.lang.IllegalArgumentException: Connection to XXX.XX.XXX.XXX:15000 not allowed. This connection point is not known in the cluster viewConnection to XXX.XX.XXX.XXX:15000 not allowed. This connection point is not known in the cluster view
原本打算看一下是否是代碼邏輯問題導致redis負載過高,于是登錄redis服務器使用monitor命令觀察命令執行的頻率,發現每執行一次命令都執行一次Auth password
命令,說明連接池未正確使用導致執行一次命令創建一次連接,導致負載高 ,并且代碼執行效率低 。
然后對比了使用JedisCluster的項目沒有此類問題,因此懷疑是spring-boot-starter-data-redis的RedisTemplate的問題,查看源碼后發現spring-data-redis的驅動包在某個版本之后替換為 Lettuce,在啟用集群后jedis的連接池無效。錯誤配置如下:
# 錯誤配置# Redis配置spring.redis.cluster.nodes=127.0.0.1:6379### 連接超時時間(毫秒)spring.redis.timeout=60000spring.redis.password=xxxxxxx# 連接池最大連接數(使用負值表示沒有限制)spring.redis.jedis.pool.max-active=8##連接池最大阻塞等待時間,若使用負值表示沒有限制spring.redis.jedis.pool.max-wait=-1##連接池中的最大空閑連接spring.redis.jedis.pool.max-idle=8# 連接池中的最小空閑連接spring.redis.jedis.pool.min-idle=0
需要改成正確的配置,修改之后無此現象,具體配置如下:
單機版:
# 單機版# Redis配置spring.redis.host=127.0.0.1spring.redis.port=6379### 連接超時時間(毫秒)spring.redis.timeout=60000spring.redis.password=xxxxxxx# 連接池最大連接數(使用負值表示沒有限制)spring.redis.jedis.pool.max-active=8##連接池最大阻塞等待時間,若使用負值表示沒有限制spring.redis.jedis.pool.max-wait=-1##連接池中的最大空閑連接spring.redis.jedis.pool.max-idle=8# 連接池中的最小空閑連接spring.redis.jedis.pool.min-idle=0
集群版:
#集群版 # Redis配置spring.redis.cluster.nodes=127.0.0.1:6379### 連接超時時間(毫秒)spring.redis.timeout=60000spring.redis.password=xxxxxxx# 連接池最大連接數(使用負值表示沒有限制)spring.redis.lettuce.pool.max-active=8##連接池最大阻塞等待時間,若使用負值表示沒有限制spring.redis.lettuce.pool.max-wait=-1##連接池中的最大空閑連接spring.redis.lettuce.pool.max-idle=8# 連接池中的最小空閑連接spring.redis.lettuce.pool.min-idle=0
注意:啟用集群版,需要額外添加如下依賴
org.apache.commons commons-pool2 2.8.0
網上搜索了一下,發現項目github上已有此問題的反饋以及解決辦法github.com/lettuce-io/…,原因是由于Lettuce其中有個配置項validateClusterNodeMembership
默認是true導致;
由于spring boot未能直接通過配置文件直接修改此配置,因此需要自定義Redis配置,具體代碼如下: MylettuceConnectionFactory.java
package com.quison.test.config;import io.lettuce.core.AbstractRedisClient;import io.lettuce.core.cluster.ClusterClientOptions;import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;import io.lettuce.core.cluster.RedisClusterClient;import org.springframework.beans.DirectFieldAccessor;import org.springframework.data.redis.connection.RedisClusterConfiguration;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 java.util.concurrent.TimeUnit;public class MyLettuceConnectionFactory extends LettuceConnectionFactory { public MyLettuceConnectionFactory() { } public MyLettuceConnectionFactory(RedisClusterConfiguration redisClusterConfiguration, LettuceClientConfiguration lettuceClientConfiguration) { super(redisClusterConfiguration, lettuceClientConfiguration); } @Override public void afterPropertiesSet() { super.afterPropertiesSet(); DirectFieldAccessor accessor = new DirectFieldAccessor(this); AbstractRedisClient client = (AbstractRedisClient) accessor.getPropertyValue("client"); if(client instanceof RedisClusterClient){ RedisClusterClient clusterClient = (RedisClusterClient) client; ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() .enablePeriodicRefresh(10, TimeUnit.MINUTES) .enableAllAdaptiveRefreshTriggers() .build(); ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder() // 注意此配置項設置為false .validateClusterNodeMembership(false) .topologyRefreshOptions(topologyRefreshOptions) .build(); clusterClient.setOptions(clusterClientOptions); } }}
由于配置后,連接池也需要自行設置,因此Redis的配置文件修改為如下設置 RedisConfig.java
package com.quison.test.config;import org.apache.commons.pool2.impl.GenericObjectPoolConfig;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;import org.springframework.boot.autoconfigure.data.redis.RedisProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.data.redis.connection.*;import org.springframework.data.redis.connection.lettuce.DefaultLettucePool;import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.RedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;import java.util.Arrays;import java.util.HashSet;import java.util.Set;@Configurationpublic class RedisConfig { @Value("${spring.redis.cluster.nodes}") private String clusterNodes; @Value("${spring.redis.password}") private String password; @Value("${spring.redis.lettuce.pool.max-idle}") private Integer maxIdle; @Value("${spring.redis.lettuce.pool.max-active}") private Integer maxActive; @Value("${spring.redis.cluster.max-redirects}") private Integer maxRedirects; @Bean public RedisConnectionFactory myRedisConnectionFactory() { RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(); String[] serverArray = clusterNodes.split(","); Set nodes = new HashSet(); for (String ipPort : serverArray) { String[] ipAndPort = ipPort.split(":"); nodes.add(new RedisNode(ipAndPort[0].trim(), Integer.valueOf(ipAndPort[1]))); } redisClusterConfiguration.setPassword(RedisPassword.of(password)); redisClusterConfiguration.setClusterNodes(nodes); redisClusterConfiguration.setMaxRedirects(maxRedirects); GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig(); genericObjectPoolConfig.setMaxIdle(maxIdle); genericObjectPoolConfig.setMinIdle(8); genericObjectPoolConfig.setMaxTotal(maxActive); genericObjectPoolConfig.setMaxWaitMillis(10000); LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() .commandTimeout(Duration.ofMillis(10000)) .poolConfig(genericObjectPoolConfig) .build(); return new MyLettuceConnectionFactory(redisClusterConfiguration, clientConfig); } /** * redis模板,存儲關鍵字是字符串,值是Jdk序列化 * * @param myRedisConnectionFactory * @return * @Description: */ @Bean @ConditionalOnMissingBean(name = "redisTemplate") @Primary public RedisTemplate, ?> redisTemplate(RedisConnectionFactory myRedisConnectionFactory) { RedisTemplate, ?> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(myRedisConnectionFactory); //key序列化方式;但是如果方法上有Long等非String類型的話,會報類型轉換錯誤; RedisSerializer redisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(redisSerializer); redisTemplate.setHashKeySerializer(redisSerializer); //默認使用JdkSerializationRedisSerializer序列化方式;會出現亂碼,改成StringRedisSerializer StringRedisSerializer stringSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringSerializer); redisTemplate.setValueSerializer(stringSerializer); redisTemplate.setHashKeySerializer(stringSerializer); redisTemplate.setHashValueSerializer(stringSerializer); return redisTemplate; }}
吃一塹、長一智,總結如下:
開發+測試環境盡量與線上一致,可提前發現問題;使用新技術需要多多測試再投入生產使用;標簽: