一文详解SpringBoot Redis多数据源配置

 更新时间:2024年11月17日 09:33:01   作者:Lvan  
Spring Boot默认只允许一种 Redis 连接池配置,且配置受限于 Lettuce 包,不够灵活,所以本文将为大家介绍如何自定义Redis配置方案实现多数据源支持,需要的可以参考下

问题背景

在实际项目中,我们需要支持两种 Redis 部署方式(集群和主从),但 Spring Boot 默认只允许一种 Redis 连接池配置,且配置受限于 Lettuce 包,不够灵活。为了解决这个问题,我深入研究了 Spring Boot 的源码,自定义了 Redis 配置方案,实现了多数据源支持。这一调整让我们可以在同一项目中灵活适配不同的 Redis 部署方式,解决了配置的局限性,让系统更具扩展性和灵活性。

源码分析

LettuceConnectionConfiguration 的核心配置

LettuceConnectionConfiguration 位于 org.springframework.boot.autoconfigure.data.redis 包内,使其作用域被限制,无法直接扩展。为支持集群和主从 Redis 部署,我们需要通过自定义配置,绕过该限制。

LettuceConnectionFactory 是 Redis 连接的核心工厂,依赖于 DefaultClientResources,并通过 LettuceClientConfiguration 设置诸如连接池、超时时间等基础参数。配置如下:

class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
	@Bean(destroyMethod = "shutdown")
	@ConditionalOnMissingBean(ClientResources.class)
	DefaultClientResources lettuceClientResources(ObjectProvider<ClientResourcesBuilderCustomizer> customizers) {
		DefaultClientResources.Builder builder = DefaultClientResources.builder();
		customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
		return builder.build();
	}

	@Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	LettuceConnectionFactory redisConnectionFactory(
			ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
			ClientResources clientResources) {
		LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
				getProperties().getLettuce().getPool());
		return createLettuceConnectionFactory(clientConfig);
	}
}

lettuceClientResources 方法定义了 ClientResources,作为单例供所有 Redis 连接工厂复用。因此,自定义 LettuceConnectionFactory 时可以直接使用这个共享的 ClientResources

客户端配置与初始化解析

LettuceClientConfiguration 的获取getLettuceClientConfiguration 方法用以构建 Lettuce 客户端配置,应用基本参数并支持连接池:

private LettuceClientConfiguration getLettuceClientConfiguration(
       ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,  
       ClientResources clientResources, Pool pool) {
    LettuceClientConfigurationBuilder builder = createBuilder(pool);
    applyProperties(builder);
    if (StringUtils.hasText(getProperties().getUrl())) {  
       customizeConfigurationFromUrl(builder);  
    }
    builder.clientOptions(createClientOptions());
    builder.clientResources(clientResources);
    builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
    return builder.build();
}

创建 LettuceClientConfigurationBuildercreateBuilder 方法生成 LettuceClientConfigurationBuilder,并判断是否启用连接池。若启用,PoolBuilderFactory 会创建包含连接池的配置,该连接池通过 GenericObjectPoolConfig 构建。

private static final boolean COMMONS_POOL2_AVAILABLE = ClassUtils.isPresent("org.apache.commons.pool2.ObjectPool", 
       RedisConnectionConfiguration.class.getClassLoader());
       
private LettuceClientConfigurationBuilder createBuilder(Pool pool) {  
    if (isPoolEnabled(pool)) {  
       return new PoolBuilderFactory().createBuilder(pool);  
    }  
    return LettuceClientConfiguration.builder();  
}

protected boolean isPoolEnabled(Pool pool) {  
    Boolean enabled = pool.getEnabled();  
    return (enabled != null) ? enabled : COMMONS_POOL2_AVAILABLE;  
}

private static class PoolBuilderFactory {  
  
    LettuceClientConfigurationBuilder createBuilder(Pool properties) {  
       return LettucePoolingClientConfiguration.builder().poolConfig(getPoolConfig(properties));  
    }  
  
    private GenericObjectPoolConfig<?> getPoolConfig(Pool properties) {  
       GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();  
       config.setMaxTotal(properties.getMaxActive());  
       config.setMaxIdle(properties.getMaxIdle());  
       config.setMinIdle(properties.getMinIdle());  
       if (properties.getTimeBetweenEvictionRuns() != null) {  
          config.setTimeBetweenEvictionRuns(properties.getTimeBetweenEvictionRuns());  
       }  
       if (properties.getMaxWait() != null) {  
          config.setMaxWait(properties.getMaxWait());  
       }  
       return config;  
    }   
}

参数应用与超时配置applyProperties 方法用于配置 Redis 的基础属性,如 SSL、超时时间等。

private LettuceClientConfigurationBuilder applyProperties(
    LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) {
    if (getProperties().isSsl()) {
        builder.useSsl();
    }
    if (getProperties().getTimeout() != null) {
        builder.commandTimeout(getProperties().getTimeout());
    }
    if (getProperties().getLettuce() != null) {
        RedisProperties.Lettuce lettuce = getProperties().getLettuce();
        if (lettuce.getShutdownTimeout() != null && !lettuce.getShutdownTimeout().isZero()) {
            builder.shutdownTimeout(getProperties().getLettuce().getShutdownTimeout());
        }
    }
    if (StringUtils.hasText(getProperties().getClientName())) {
        builder.clientName(getProperties().getClientName());
    }
    return builder;
}

Redis 多模式支持

在创建 LettuceConnectionFactory 时,根据不同配置模式(哨兵、集群或单节点)构建连接工厂:

private LettuceConnectionFactory createLettuceConnectionFactory(LettuceClientConfiguration clientConfiguration) {
    if (getSentinelConfig() != null) {
        return new LettuceConnectionFactory(getSentinelConfig(), clientConfiguration);
    }
    if (getClusterConfiguration() != null) {
        return new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration);
    }
    return new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
}
  • 哨兵模式getSentinelConfig() 返回哨兵配置实例,实现高可用 Redis。
  • 集群模式:若启用集群,getClusterConfiguration() 返回集群配置以支持分布式 Redis。
  • 单节点模式:默认单节点配置,返回 StandaloneConfig

getClusterConfiguration 的方法实现:

protected final RedisClusterConfiguration getClusterConfiguration() {
    if (this.clusterConfiguration != null) {
        return this.clusterConfiguration;
    }
    if (this.properties.getCluster() == null) {
        return null;
    }
    RedisProperties.Cluster clusterProperties = this.properties.getCluster();
    RedisClusterConfiguration config = new RedisClusterConfiguration(clusterProperties.getNodes());
    if (clusterProperties.getMaxRedirects() != null) {
        config.setMaxRedirects(clusterProperties.getMaxRedirects());
    }
    config.setUsername(this.properties.getUsername());
    if (this.properties.getPassword() != null) {
        config.setPassword(RedisPassword.of(this.properties.getPassword()));
    }
    return config;
}
  • 集群节点与重定向:配置集群节点信息及最大重定向次数。
  • 用户名与密码:集群连接的身份验证配置。

Redis 多数据源配置思路

通过以上的源码分析,我梳理出了 LettuceConnectionFactory 构建的流程:

1.LettuceClientConfiguration 初始化:

  • 连接池初始化
  • Redis 基础属性配置
  • 设置 ClientResource

2.RedisConfiguration 初始化,官方支持以下配置:

  • RedisClusterConfiguration
  • RedisSentinelConfiguration
  • RedisStaticMasterReplicaConfiguration
  • RedisStandaloneConfiguration

Redis 多数据源配置实战

复用 LettuceClientConfiguration 配置

/**
 * 这里与源码有个不同的是返回了builder,因为后续实现的主从模式的lettuce客户端仍有自定义的配置,返回了builder则相对灵活一点。
 */
private LettuceClientConfiguration.LettuceClientConfigurationBuilder getLettuceClientConfiguration(ClientResources clientResources, RedisProperties.Pool pool) {  
    LettuceClientConfiguration.LettuceClientConfigurationBuilder builder = createBuilder(pool);  
    applyProperties(builder);  
    builder.clientOptions(createClientOptions());  
    builder.clientResources(clientResources);  
    return builder;  
}  
  
/**  
 * 创建Lettuce客户端配置构建器  
 */  
private LettuceClientConfiguration.LettuceClientConfigurationBuilder createBuilder(RedisProperties.Pool pool) {  
    if (isPoolEnabled(pool)) {  
        return LettucePoolingClientConfiguration.builder().poolConfig(getPoolConfig(pool));  
    }  
    return LettuceClientConfiguration.builder();  
}  
  
/**  
 * 判断Redis连接池是否启用  
 */  
private boolean isPoolEnabled(RedisProperties.Pool pool) {  
    Boolean enabled = pool.getEnabled();  
    return (enabled != null) ? enabled : COMMONS_POOL2_AVAILABLE;  
}  
  
/**  
 * 根据Redis属性配置创建并返回一个通用对象池配置  
 */  
private GenericObjectPoolConfig<?> getPoolConfig(RedisProperties.Pool properties) {  
    GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();  
    config.setMaxTotal(properties.getMaxActive());  
    config.setMaxIdle(properties.getMaxIdle());  
    config.setMinIdle(properties.getMinIdle());  
    if (properties.getTimeBetweenEvictionRuns() != null) {  
        config.setTimeBetweenEvictionRuns(properties.getTimeBetweenEvictionRuns());  
    }  
    if (properties.getMaxWait() != null) {  
        config.setMaxWait(properties.getMaxWait());  
    }  
    return config;  
}  
  
/**  
 * 根据Redis属性配置构建Lettuce客户端配置  
 *  
 * @param builder Lettuce客户端配置的构建器  
 * @return 返回配置完毕的Lettuce客户端配置构建器  
 */  
private LettuceClientConfiguration.LettuceClientConfigurationBuilder applyProperties(  
        LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) {  
    if (redisProperties.isSsl()) {  
        builder.useSsl();  
    }  
    if (redisProperties.getTimeout() != null) {  
        builder.commandTimeout(redisProperties.getTimeout());  
    }  
    if (redisProperties.getLettuce() != null) {  
        RedisProperties.Lettuce lettuce = redisProperties.getLettuce();  
        if (lettuce.getShutdownTimeout() != null && !lettuce.getShutdownTimeout().isZero()) {  
            builder.shutdownTimeout(redisProperties.getLettuce().getShutdownTimeout());  
        }  
    }  
    if (StringUtils.hasText(redisProperties.getClientName())) {  
        builder.clientName(redisProperties.getClientName());  
    }  
    return builder;  
}  
  
/**  
 * 创建客户端配置选项  
 */  
private ClientOptions createClientOptions() {  
    ClientOptions.Builder builder = initializeClientOptionsBuilder();  
    Duration connectTimeout = redisProperties.getConnectTimeout();  
    if (connectTimeout != null) {  
        builder.socketOptions(SocketOptions.builder().connectTimeout(connectTimeout).build());  
    }  
    return builder.timeoutOptions(TimeoutOptions.enabled()).build();  
}  
  
/**  
 * 初始化ClientOptions构建器  
 */  
private ClientOptions.Builder initializeClientOptionsBuilder() {  
    if (redisProperties.getCluster() != null) {  
        ClusterClientOptions.Builder builder = ClusterClientOptions.builder();  
        RedisProperties.Lettuce.Cluster.Refresh refreshProperties = redisProperties.getLettuce().getCluster().getRefresh();  
        ClusterTopologyRefreshOptions.Builder refreshBuilder = ClusterTopologyRefreshOptions.builder()  
                .dynamicRefreshSources(refreshProperties.isDynamicRefreshSources());  
        if (refreshProperties.getPeriod() != null) {  
            refreshBuilder.enablePeriodicRefresh(refreshProperties.getPeriod());  
        }  
        if (refreshProperties.isAdaptive()) {  
            refreshBuilder.enableAllAdaptiveRefreshTriggers();  
        }  
        return builder.topologyRefreshOptions(refreshBuilder.build());  
    }  
    return ClientOptions.builder();  
}

复用 Redis 集群初始化

/**  
 * 获取Redis集群配置  
 */  
private RedisClusterConfiguration getClusterConfiguration() {  
    if (redisProperties.getCluster() == null) {  
        return null;  
    }  
    RedisProperties.Cluster clusterProperties = redisProperties.getCluster();  
    RedisClusterConfiguration config = new RedisClusterConfiguration(clusterProperties.getNodes());  
    if (clusterProperties.getMaxRedirects() != null) {  
        config.setMaxRedirects(clusterProperties.getMaxRedirects());  
    }  
    config.setUsername(redisProperties.getUsername());  
    if (redisProperties.getPassword() != null) {  
        config.setPassword(RedisPassword.of(redisProperties.getPassword()));  
    }  
    return config;  
}

自定义 Redis 主从配置

@Getter  
@Setter  
@ConfigurationProperties(prefix = "spring.redis")  
public class RedisPropertiesExtend {  
  
    private MasterReplica masterReplica;  
  
    @Getter  
    @Setter    public static class MasterReplica {  
  
        private String masterNodes;  
        private List<String> replicaNodes;  
    }  
}

/**  
 * 获取主从配置  
 */  
private RedisStaticMasterReplicaConfiguration getStaticMasterReplicaConfiguration() {  
    if (redisPropertiesExtend.getMasterReplica() == null) {  
        return null;  
    }  
    RedisPropertiesExtend.MasterReplica masterReplica = redisPropertiesExtend.getMasterReplica();  
    List<String> masterNodes = CharSequenceUtil.split(masterReplica.getMasterNodes(), StrPool.COLON);  
    RedisStaticMasterReplicaConfiguration config = new RedisStaticMasterReplicaConfiguration(  
            masterNodes.get(0), Integer.parseInt(masterNodes.get(1)));  
    for (String replicaNode : masterReplica.getReplicaNodes()) {  
        List<String> replicaNodes = CharSequenceUtil.split(replicaNode, StrPool.COLON);  
        config.addNode(replicaNodes.get(0), Integer.parseInt(replicaNodes.get(1)));  
    }  
    config.setUsername(redisProperties.getUsername());  
    if (redisProperties.getPassword() != null) {  
        config.setPassword(RedisPassword.of(redisProperties.getPassword()));  
    }  
    return config;  
}

注册 LettuceConnectionFactory

@Primary  
@Bean(name = "redisClusterConnectionFactory")  
@ConditionalOnMissingBean(name = "redisClusterConnectionFactory")  
public LettuceConnectionFactory redisClusterConnectionFactory(ClientResources clientResources) {  
    LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(clientResources,  
            redisProperties.getLettuce().getPool())  
            .build();  
    return new LettuceConnectionFactory(getClusterConfiguration(), clientConfig);  
}  
  
@Bean(name = "redisMasterReplicaConnectionFactory")  
@ConditionalOnMissingBean(name = "redisMasterReplicaConnectionFactory")  
public LettuceConnectionFactory redisMasterReplicaConnectionFactory(ClientResources clientResources) {  
    LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(clientResources,  
            redisProperties.getLettuce().getPool())  
            .readFrom(ReadFrom.REPLICA_PREFERRED)  
            .build();  
    return new LettuceConnectionFactory(getStaticMasterReplicaConfiguration(), clientConfig);  
}

应用

@Bean  
@ConditionalOnMissingBean(name = "redisTemplate")  
public RedisTemplate<Object, Object> redisTemplate(@Qualifier("redisClusterConnectionFactory") RedisConnectionFactory redisConnectionFactory) {  
    RedisTemplate<Object, Object> template = new RedisTemplate<>();  
    template.setConnectionFactory(redisConnectionFactory);  
    return template;  
}

@Bean  
@ConditionalOnMissingBean(name = "masterReplicaRedisTemplate")  
public RedisTemplate<Object, Object> masterReplicaRedisTemplate(@Qualifier("redisMasterReplicaConnectionFactory") RedisConnectionFactory redisConnectionFactory) {  
    RedisTemplate<Object, Object> template = new RedisTemplate<>();  
    template.setConnectionFactory(redisConnectionFactory);  
    return template;  
}

最后

深入理解 Spring Boot Redis 自动配置源码是实现灵活多数据源支持的关键。通过源码分析,我们能够在保持框架兼容性的同时,精准定制和扩展功能,从而满足复杂的业务需求。

以上就是一文详解SpringBoot Redis多数据源配置的详细内容,更多关于SpringBoot Redis多数据源配置的资料请关注脚本之家其它相关文章!

相关文章

  • JAVA工程中引用本地jar的3种常用简单方式

    JAVA工程中引用本地jar的3种常用简单方式

    Jar文件的全称是Java Archive File即Java归档文件,主要是对class文件进行压缩,是一种压缩文件,和常见的zip压缩文件兼容,下面这篇文章主要给大家介绍了关于JAVA工程中引用本地jar的3种常用简单方式,需要的朋友可以参考下
    2024-03-03
  • Java集合系列之HashMap源码分析

    Java集合系列之HashMap源码分析

    这篇文章主要为大家详细介绍了Java集合系列之HashMap源码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-02-02
  • Java求余%操作引发的一连串故事

    Java求余%操作引发的一连串故事

    取模运算与取余运算两个概念有重叠的部分但又不完全一致。主要的区别在于对负整数进行除法运算时操作不同。本文重点给大家介绍Java求余%操作引发的一连串故事,感兴趣的朋友跟随小编一起看看吧
    2021-05-05
  • Java中System.currentTimeMillis()计算方式与时间单位转换讲解

    Java中System.currentTimeMillis()计算方式与时间单位转换讲解

    本文详细讲解了Java中System.currentTimeMillis()计算方式与时间单位转换,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-12-12
  • springBoot整合CXF并实现用户名密码校验的方法

    springBoot整合CXF并实现用户名密码校验的方法

    这篇文章主要介绍了springBoot整合CXF并实现用户名密码校验的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-08-08
  • Java实现读取设置pdf属性信息

    Java实现读取设置pdf属性信息

    这篇文章主要为大家详细介绍了如何使用Java实现读取设置pdf属性信息,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2025-01-01
  • 将Java程序打包成EXE文件的实现方式

    将Java程序打包成EXE文件的实现方式

    这篇文章主要介绍了将Java程序打包成EXE文件的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-04-04
  • 一文详解如何更改电脑使用的JDK版本

    一文详解如何更改电脑使用的JDK版本

    我们在日常学习或者工作中,难免会遇到需要使用不同的jdk版本进行开发,这篇文章主要给大家介绍了关于如何更改电脑使用的JDK版本的相关资料,需要的朋友可以参考下
    2024-01-01
  • JAVA最容易忽视的数据类型之枚举详解

    JAVA最容易忽视的数据类型之枚举详解

    这篇文章主要给大家介绍了关于JAVA最容易忽视的数据类型之枚举的相关资料,Java中的枚举类型是一种特殊的类型,它允许程序员定义一个固定的值集合,并为每个值分配一个名称,枚举类型提供了一种简单、安全和可读性强的方式来表示一组相关的常量,需要的朋友可以参考下
    2023-10-10
  • Java语言中的内存泄露代码详解

    Java语言中的内存泄露代码详解

    这篇文章主要介绍了Java语言中的内存泄露代码详解,具有一定借鉴价值,需要的朋友可以参考下。
    2017-12-12

最新评论