在SpringBoot项目中优雅的连接多台Redis的操作方法
作者:CavenWang
如何在SpringBoot项目中优雅的连接多台Redis
在Spring Boot项目中,连接单个Redis实例是常见需求,但有时需要同时连接多个Redis实例(例如,主Redis用于业务数据存储,另一个Redis用于爬虫数据缓存)。本文将基于一个实际案例,详细介绍如何在Spring Boot中优雅地配置和使用多个Redis实例,解决常见的注入歧义问题,并提供一个通用的Redis工具类来简化操作。
背景
在一个Spring Boot项目中,我们需要连接两台Redis实例:
- 主Redis:用于常规业务数据,配置在
spring.redis
(数据库索引4)。 - 爬虫Redis:用于爬虫相关数据,配置在
spring.redis-crawler
(数据库索引7)。
项目中遇到以下问题:
- 配置两个
RedisTemplate
时,Spring容器找不到redisCrawlerConnectionFactory
。 - 配置多个
RedisProperties
导致注入歧义,抛出NoUniqueBeanDefinitionException
。 - 需要一个通用的
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
)。
解决方法:
- 注入两个
RedisTemplate
(redisTemplate
和redisTemplateCrawl
)。 - 保留原有方法,默认使用
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
注入redisTemplate
和redisTemplateCrawl
。 - 保留原有方法(如
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。
优化建议
使用枚举替代字符串:
为避免templateName
的硬编码,可使用枚举:
public enum RedisInstance { DEFAULT, CRAWL } private RedisTemplate getRedisTemplate(RedisInstance instance) { return instance == RedisInstance.CRAWL ? redisTemplateCrawl : redisTemplate; }
使用示例:
redisCache.setCacheObject(RedisInstance.CRAWL, "key2", "value2");
错误处理:
在getRedisTemplate
中添加空检查:
private RedisTemplate getRedisTemplate(String templateName) { if (redisTemplate == null || redisTemplateCrawl == null) { throw new IllegalStateException("RedisTemplate not initialized"); } return "crawl".equalsIgnoreCase(templateName) ? redisTemplateCrawl : redisTemplate; }
连接测试:
确保Redis服务器可访问(此处IP已脱敏):
redis-cli -h [REDACTED_HOST] -p 6379 -n 4 # 主Redis redis-cli -h [REDACTED_HOST] -p 6379 -n 7 # 爬虫Redis
序列化器:
确保FastJson2JsonRedisSerializer
实现正确,支持序列化和反序列化。
总结
通过以下步骤,我们在Spring Boot项目中实现了优雅的多Redis连接:
- 在
application.yml
中配置两套Redis(spring.redis
和spring.redis-crawler
)。 - 在
RedisConfig.java
中定义两个RedisProperties
、RedisConnectionFactory
和RedisTemplate
,使用@Primary
和@Qualifier
解决注入歧义。 - 实现
RedisCache
工具类,支持动态选择Redis实例,同时兼容现有代码。
这种方案既灵活又易于扩展,适合需要操作多个Redis实例的场景。如果你有更多Redis实例,只需重复上述步骤,定义新的RedisProperties
和RedisTemplate
,并在RedisCache
中扩展支持。
参考:
- Spring Boot官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.redis
- Lettuce官方文档:https://lettuce.io/
到此这篇关于在SpringBoot项目中优雅的连接多台Redis的操作方法的文章就介绍到这了,更多相关SpringBoot连接多台Redis内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章:
- SpringBoot连接Redis集群教程
- SpringBoot实现自定义Redis的连接的流程步骤
- 关于SpringBoot集成Lettuce连接Redis的方法和案例
- springboot连接不上redis的三种解决办法
- springboot连接redis并动态切换database的实现方法
- springboot 如何使用jedis连接Redis数据库
- 关于Springboot2.x集成lettuce连接redis集群报超时异常Command timed out after 6 second(s)
- springboot连接Redis的教程详解
- springboot2整合redis使用lettuce连接池的方法(解决lettuce连接池无效问题)