如何在SpringBoot项目中优雅的连接多台Redis

如何在SpringBoot项目中优雅的连接多台Redis

在Spring Boot项目中,连接单个Redis实例是常见需求,但有时需要同时连接多个Redis实例(例如,主Redis用于业务数据存储,另一个Redis用于爬虫数据缓存)。本文将基于一个实际案例,详细介绍如何在Spring Boot中优雅地配置和使用多个Redis实例,解决常见的注入歧义问题,并提供一个通用的Redis工具类来简化操作。


背景

在一个Spring Boot项目中,我们需要连接两台Redis实例:

  • 主Redis:用于常规业务数据,配置在spring.redis(数据库索引4)。
  • 爬虫Redis:用于爬虫相关数据,配置在spring.redis-crawler(数据库索引7)。

项目中遇到以下问题:

  1. 配置两个RedisTemplate时,Spring容器找不到redisCrawlerConnectionFactory
  2. 配置多个RedisProperties导致注入歧义,抛出NoUniqueBeanDefinitionException
  3. 需要一个通用的RedisCache工具类,支持动态选择Redis实例,同时兼容现有代码。

下面,我们将逐步解决这些问题,并展示如何优雅地实现多Redis连接。


项目配置

1. 配置文件(application.yml)

首先,在application.yml中定义两套Redis配置:

spring:
  redis:
    host: [REDACTED_HOST]
    port: 6379
    database: 4
    # password: [REDACTED_PASSWORD]
    timeout: 10s
    lettuce:
      pool:
        min-idle: 0
        max-idle: 8
        max-active: 8
        max-wait: -1ms
  redis-crawler:
    host: [REDACTED_HOST]
    port: 6379
    database: 7
    # password: [REDACTED_PASSWORD]
    timeout: 10s
    lettuce:
      pool:
        min-idle: 0
        max-idle: 8
        max-active: 8
        max-wait: -1ms
  • spring.redis:主Redis,数据库索引4。
  • spring.redis-crawler:爬虫Redis,数据库索引7。
  • 如果Redis需要密码,取消password字段的注释并设置正确值(此处已脱敏)。

2. 依赖配置(pom.xml)

确保项目包含以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.51</version>
</dependency>
  • spring-boot-starter-data-redis:提供Redis支持。
  • lettuce-core:使用Lettuce作为Redis客户端。
  • fastjson:用于自定义序列化(项目中使用了FastJson2JsonRedisSerializer)。

配置多个Redis实例

问题1:找不到redisCrawlerConnectionFactory

最初,我们尝试在RedisConfig.java中定义两个RedisTemplate

@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    // 主Redis模板
}

@Bean(name = "redisTemplateCrawl")
public RedisTemplate<Object, Object> redisTemplateCrawl(@Qualifier("redisCrawlerConnectionFactory") RedisConnectionFactory redisCrawlerConnectionFactory) {
    // 爬虫Redis模板
}

启动时抛出异常:

No qualifying bean of type 'org.springframework.data.redis.connection.RedisConnectionFactory' available

原因:Spring Boot自动为spring.redis创建了一个RedisConnectionFactory,但不会为spring.redis-crawler创建。redisTemplateCrawl依赖的redisCrawlerConnectionFactory未定义。

解决方法:显式定义redisCrawlerConnectionFactory和对应的RedisProperties

@Bean(name = "redisCrawlerProperties")
@ConfigurationProperties(prefix = "spring.redis-crawler")
public RedisProperties redisCrawlerProperties() {
    return new RedisProperties();
}

@Bean(name = "redisCrawlerConnectionFactory")
public RedisConnectionFactory redisCrawlerConnectionFactory(@Qualifier("redisCrawlerProperties") RedisProperties redisCrawlerProperties) {
    RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
    config.setHostName(redisCrawlerProperties.getHost());
    config.setPort(redisCrawlerProperties.getPort());
    config.setDatabase(redisCrawlerProperties.getDatabase());
    if (redisCrawlerProperties.getPassword() != null) {
        config.setPassword(redisCrawlerProperties.getPassword());
    }
    return new LettuceConnectionFactory(config);
}
  • redisCrawlerProperties:绑定spring.redis-crawler配置。
  • redisCrawlerConnectionFactory:根据redisCrawlerProperties创建连接工厂。

问题2:RedisProperties注入歧义

配置了redisCrawlerProperties后,启动时又遇到新问题:

Parameter 0 of constructor in org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration required a single bean, but 2 were found:
	- redisCrawlerProperties
	- spring.redis-org.springframework.boot.autoconfigure.data.redis.RedisProperties

原因:Spring Boot自动为spring.redis创建了一个RedisProperties,而我们又定义了redisCrawlerProperties,导致LettuceConnectionConfiguration无法确定使用哪个RedisProperties

解决方法:显式定义主Redis的RedisProperties,并用@Primary标记为默认:

@Bean(name = "redisProperties")
@Primary
@ConfigurationProperties(prefix = "spring.redis")
public RedisProperties redisProperties() {
    return new RedisProperties();
}

@Bean(name = "redisConnectionFactory")
public RedisConnectionFactory redisConnectionFactory(@Qualifier("redisProperties") RedisProperties redisProperties) {
    RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
    config.setHostName(redisProperties.getHost());
    config.setPort(redisProperties.getPort());
    config.setDatabase(redisProperties.getDatabase());
    if (redisProperties.getPassword() != null) {
        config.setPassword(redisProperties.getPassword());
    }
    return new LettuceConnectionFactory(config);
}
  • redisProperties:绑定spring.redis,用@Primary标记为默认。
  • redisConnectionFactory:为主Redis创建连接工厂,确保redisTemplate使用正确配置。

最终的RedisConfig.java

以下是完整的RedisConfig.java(敏感信息已脱敏):

package com.caven.framework.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Bean(name = "redisProperties")
    @Primary
    @ConfigurationProperties(prefix = "spring.redis")
    public RedisProperties redisProperties() {
        return new RedisProperties();
    }

    @Bean(name = "redisConnectionFactory")
    public RedisConnectionFactory redisConnectionFactory(@Qualifier("redisProperties") RedisProperties redisProperties) {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName(redisProperties.getHost());
        config.setPort(redisProperties.getPort());
        config.setDatabase(redisProperties.getDatabase());
        if (redisProperties.getPassword() != null) {
            config.setPassword(redisProperties.getPassword());
        }
        return new LettuceConnectionFactory(config);
    }

    @Bean(name = "redisTemplate")
    @SuppressWarnings(value = {"unchecked", "rawtypes"})
    public RedisTemplate<Object, Object> redisTemplate(@Qualifier("redisConnectionFactory") RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean(name = "redisCrawlerProperties")
    @ConfigurationProperties(prefix = "spring.redis-crawler")
    public RedisProperties redisCrawlerProperties() {
        return new RedisProperties();
    }

    @Bean(name = "redisCrawlerConnectionFactory")
    public RedisConnectionFactory redisCrawlerConnectionFactory(@Qualifier("redisCrawlerProperties") RedisProperties redisCrawlerProperties) {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName(redisCrawlerProperties.getHost());
        config.setPort(redisCrawlerProperties.getPort());
        config.setDatabase(redisCrawlerProperties.getDatabase());
        if (redisCrawlerProperties.getPassword() != null) {
            config.setPassword(redisCrawlerProperties.getPassword());
        }
        return new LettuceConnectionFactory(config);
    }

    @Bean(name = "redisTemplateCrawl")
    @SuppressWarnings(value = {"unchecked", "rawtypes"})
    public RedisTemplate<Object, Object> redisTemplateCrawl(@Qualifier("redisCrawlerConnectionFactory") RedisConnectionFactory redisCrawlerConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisCrawlerConnectionFactory);
        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public DefaultRedisScript<Long> limitScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(limitScriptText());
        redisScript.setResultType(Long.class);
        return redisScript;
    }

    private String limitScriptText() {
        return "local key = KEYS[1]\n" +
               "local count = tonumber(ARGV[1])\n" +
               "local time = tonumber(ARGV[2])\n" +
               "local current = redis.call('get', key);\n" +
               "if current and tonumber(current) > count then\n" +
               "    return tonumber(current);\n" +
               "end\n" +
               "current = redis.call('incr', key)\n" +
               "if tonumber(current) == 1 then\n" +
               "    redis.call('expire', key, time)\n" +
               "end\n" +
               "return tonumber(current);";
    }
}

实现通用的Redis工具类

为了简化Redis操作,我们创建了一个RedisCache工具类,支持动态选择RedisTemplate,同时兼容现有代码。

问题3:动态选择Redis实例

最初的RedisCache.java只注入了一个RedisTemplate

@Autowired
public RedisTemplate redisTemplate;

这导致无法操作爬虫Redis。我们希望:

  • 现有代码继续使用主Redis(redisTemplate),无需修改。
  • 新代码可以通过参数选择主Redis或爬虫Redis(redisTemplateCrawl)。

解决方法

  • 注入两个RedisTemplateredisTemplateredisTemplateCrawl)。
  • 保留原有方法,默认使用redisTemplate
  • 为每个方法添加带templateName参数的重load版本,支持选择Redis实例。

最终的RedisCache.java

package com.caven.framework.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache {

    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate redisTemplate;

    @Autowired
    @Qualifier("redisTemplateCrawl")
    private RedisTemplate redisTemplateCrawl;

    private RedisTemplate getRedisTemplate(String templateName) {
        if ("crawl".equalsIgnoreCase(templateName)) {
            return redisTemplateCrawl;
        }
        return redisTemplate;
    }

    public <T> void setCacheObject(final String key, final T value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public <T> void setCacheObject(final String templateName, final String key, final T value) {
        getRedisTemplate(templateName).opsForValue().set(key, value);
    }

    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    public <T> void setCacheObject(final String templateName, final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
        getRedisTemplate(templateName).opsForValue().set(key, value, timeout, timeUnit);
    }

    // 其他方法类似,省略完整代码
}

关键点

  • 使用@Qualifier注入redisTemplateredisTemplateCrawl
  • 保留原有方法(如setCacheObject(String key, T value)),默认使用redisTemplate
  • 新增带templateName的重载方法(如setCacheObject(String templateName, String key, T value)),支持选择Redis实例。
  • getRedisTemplate方法根据templateName返回对应的RedisTemplate"crawl"返回redisTemplateCrawl,否则返回redisTemplate)。

使用示例

Service层中:

@Service
public class MyService {

    @Autowired
    private RedisCache redisCache;

    public void example() {
        // 现有代码:默认使用主Redis (database: 4)
        redisCache.setCacheObject("key1", "value1");
        String value1 = redisCache.getCacheObject("key1");

        // 新代码:使用爬虫Redis (database: 7)
        redisCache.setCacheObject("crawl", "key2", "value2");
        String value2 = redisCache.getCacheObject("crawl", "key2");
    }
}
  • 现有代码无需修改,继续使用主Redis。
  • 新代码通过templateName="crawl"操作爬虫Redis。

优化建议

  1. 使用枚举替代字符串
    为避免templateName的硬编码,可使用枚举:

    public enum RedisInstance {
        DEFAULT,
        CRAWL
    }
    
    private RedisTemplate getRedisTemplate(RedisInstance instance) {
        return instance == RedisInstance.CRAWL ? redisTemplateCrawl : redisTemplate;
    }
    

    使用示例:

    redisCache.setCacheObject(RedisInstance.CRAWL, "key2", "value2");
    
  2. 错误处理
    getRedisTemplate中添加空检查:

    private RedisTemplate getRedisTemplate(String templateName) {
        if (redisTemplate == null || redisTemplateCrawl == null) {
            throw new IllegalStateException("RedisTemplate not initialized");
        }
        return "crawl".equalsIgnoreCase(templateName) ? redisTemplateCrawl : redisTemplate;
    }
    
  3. 连接测试
    确保Redis服务器可访问(此处IP已脱敏):

    redis-cli -h [REDACTED_HOST] -p 6379 -n 4  # 主Redis
    redis-cli -h [REDACTED_HOST] -p 6379 -n 7  # 爬虫Redis
    
  4. 序列化器
    确保FastJson2JsonRedisSerializer实现正确,支持序列化和反序列化。


总结

通过以下步骤,我们在Spring Boot项目中实现了优雅的多Redis连接:

  1. application.yml中配置两套Redis(spring.redisspring.redis-crawler)。
  2. RedisConfig.java中定义两个RedisPropertiesRedisConnectionFactoryRedisTemplate,使用@Primary@Qualifier解决注入歧义。
  3. 实现RedisCache工具类,支持动态选择Redis实例,同时兼容现有代码。

这种方案既灵活又易于扩展,适合需要操作多个Redis实例的场景。如果你有更多Redis实例,只需重复上述步骤,定义新的RedisPropertiesRedisTemplate,并在RedisCache中扩展支持。


参考

  • Spring Boot官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.redis
  • Lettuce官方文档:https://lettuce.io/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值