如何在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: 6379database: 4# password: [REDACTED_PASSWORD]timeout: 10slettuce:pool:min-idle: 0max-idle: 8max-active: 8max-wait: -1msredis-crawler:host: [REDACTED_HOST]port: 6379database: 7# password: [REDACTED_PASSWORD]timeout: 10slettuce:pool:min-idle: 0max-idle: 8max-active: 8max-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;}@Beanpublic 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 {@Autowiredprivate 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/