SpringBoot2.X與redis Lettuce整合踩坑

Hiway發表於2020-01-06

起因

最近專案上發現一個問題,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

SpringBoot2.X與redis Lettuce整合踩坑

在設定了開啟拓撲重新整理之後,可以看到我們的兩個引數值都變成了true

SpringBoot2.X與redis Lettuce整合踩坑

其中clusterTopologyRefreshActivated引數並不是根據我們增加的拓撲重新整理開啟就初始化為true的,它是在第一次使用redisTemplate的時候,在RedisClusterClient#activateTopologyRefreshIfNeeded方法中修改為true的,有興趣的可以自行debug跟蹤一下。

SpringBoot2.X與redis Lettuce整合踩坑

不用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有哪些優缺點?

Lettuce 和 Jedis 的定位都是Redis的client,所以他們當然可以直接連線redis server。

Jedis在實現上是直接連線的redis server,如果在多執行緒環境下是非執行緒安全的,這個時候只有使用連線池,為每個Jedis例項增加物理連線

Lettuce的連線是基於Netty的,連線例項(StatefulRedisConnection)可以在多個執行緒間併發訪問,應為StatefulRedisConnection是執行緒安全的,所以一個連線例項(StatefulRedisConnection)就可以滿足多執行緒環境下的併發訪問,當然這個也是可伸縮的設計,一個連線例項不夠的情況也可以按需增加連線例項。

相關文章