Spring Boot 的 Redis 設定與更改 JedisCluster 建構方式

·

3 min read

前陣子工作上需要調 Jedis 的 maxTotalRetiresDuration ,但這是 Jedis 新加的選項,還沒被整合到 Spring Data Redis。所以需要手動去做一些設定。對 Redis 在 Spring Boot 上的設定做了一些調查,這邊筆記一下。

Change the Constructor Call of JedisCluster

我需要 call 在這個PR被加上的建構子。

public JedisCluster(Set<HostAndPort> jedisClusterNode, int connectionTimeout, int soTimeout,
      int infiniteSoTimeout, int maxAttempts, String user, String password, String clientName,
      final GenericObjectPoolConfig<Jedis> poolConfig, boolean ssl,
      SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
      HostnameVerifier hostnameVerifier, JedisClusterHostAndPortMap hostAndPortMap,
      Duration maxTotalRetriesDuration)
      // ...

相關的 config 就不細談。我的方法是 extend JedisConnectionFactorycreateCluster()

class JedisConnectionFactoryWithTotalRetriesDuration extends JedisConnectionFactory {
    private final JedisClientConfig jedisClientConfig;
    private final Duration maxTotalRetriesDuration;

    // 這個 constructor 可以依你的需求設定
    public JedisConnectionFactoryWithTotalRetriesDuration(RedisClusterConfiguration clusterConfig, Duration maxTotalRetriesDuration) {
        // Call super 
        super(clusterConfig);
        // 因為 jedisClientConfig 是 Private ,得重新建一遍 (可以視你的環境改方法)
        this.jedisClientConfig = DefaultJedisClientConfig.builder().build();
        this.maxTotalRetriesDuration = maxTotalRetriesDuration;
    }

    // 這邊直接複製原檔案的主體
    @Override
    protected JedisCluster createCluster(RedisClusterConfiguration clusterConfig,
            GenericObjectPoolConfig<Connection> poolConfig) {

        Assert.notNull(clusterConfig, "Cluster configuration must not be null");

        Set<HostAndPort> hostAndPort = new HashSet<>();
        for (RedisNode node : clusterConfig.getClusterNodes()) {
            hostAndPort.add(new HostAndPort(node.getHost(), node.getPort()));
        }

        int redirects = clusterConfig.getMaxRedirects() != null ? clusterConfig.getMaxRedirects() : 5;

        // 改掉 constructor call
        return new JedisCluster(hostAndPort, this.clientConfig, redirects, maxTotalRetriesDuration, poolConfig);
    }
}

然後就只需要在 Config class 裡自己建一個 Bean 取代 Auto Config :

@Configuration
class RedisConfig {
    @Bean
    @ConditionalOnProperty(name = "spring.redis.cluster.nodes")
    public JedisConnectionFactory redisConnectionFactory() {
        var clusterConfig = // 自定義
        var duration = // 自定義
        return new JedisConnectionFactoryWithTotalRetriesDuration(clusterConfig, duration);
    }
}

這樣就成功加上了這個自訂選項了! 預設這個選項會是 connection timeout * maxAttempts 。而這個 maxAttempts 預設是 5,所以預設他在 timeout 時和其他錯誤一樣,會 retry 5次。我們想要讓他在 timeout 時不要 retry 那麼多次。

How Redis client is configured

通常在 Spring Boot ,大部分的設定都是 Auto Configuration 就可以完成。當要客製化的時候就要自己寫 Config Class 。這時候研究 Auto Configuration Class 就可以少走彎路。

Redis Autoconfiguration

首先,從 Spring Boot 的 Autoconfiguration 開始看。可以看到,這個 config 定義了兩個 Bean ,條件是沒有其他定義,以及 RedisConnectionFactory.class 的 Bean 存在。另外這個 Config import 了兩個 Config ,一個是 LettuceConnectionConfiguration 一個是 JedisConnectionConfiguration 。我們是使用 Jedis 所以就先看 Jedis 。

RedisAutoConfiguration.java on Github

@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class) // enable RedisProperties class 
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) // let's check these files later
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate") // define bean if not exists
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class) // define bean only if RedisConnectionFactory exists
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }

}

RedisTemplate 就是 Spring Data Redis 裡抽象化 Redis 介面的一個 class ,不管用的是哪套客戶端套件(Lettus, Jedis),在應用上用起來都一樣。

JedisConnectionConfiguration

首先可以看到這邊定義了一個 JedisConnectionFactory 的 Bean 。Spring Boot是在這個檔案將 Properties 設定檔讀進來的。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class })
@ConditionalOnMissingBean(RedisConnectionFactory.class) // Only load config if no connection factory is defined.
@ConditionalOnProperty(name = "spring.data.redis.client-type", havingValue = "jedis", matchIfMissing = true)
class JedisConnectionConfiguration extends RedisConnectionConfiguration {
    // omitted
    @Bean
    JedisConnectionFactory redisConnectionFactory(
            ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) {
        return createJedisConnectionFactory(builderCustomizers);
    }

AfterPropertiesSet

最後就是關於研究這個設定的主要目的: override JedisCluster 的建構子。在 Spring Boot 裡面 JedisCluster 是在前面定義的 JedisConnectionFactory 裡的 afterPropertiesSet 方法裡呼叫了 createCluster()。這個afterPropertiesSet() 是什麼?

這就發現,afterPropertiesSet() 是定義在 InitializingBean 這個 Interface 裡。解說。只要有實作這個 Interface ,在 bean 被建立後 Spring Framework 就會 call 這個方法來初始化相關屬性。

我最後就可以得到結論: 我只要想辦法改了 createCluster() 這個方法,就可以改掉建構子。

Future Improvement

其實這樣複製貼上繼承原本的 method 是不太健康所以我是有開了一個 PR 去 Spring Data Redis,想問問是不是可以直接把 maxTotalRetriesDuration 收進去選項裡,不過還沒有受理。