在SpringBoot項(xiàng)目中優(yōu)雅的連接多臺(tái)Redis的操作方法
如何在SpringBoot項(xiàng)目中優(yōu)雅的連接多臺(tái)Redis
在Spring Boot項(xiàng)目中,連接單個(gè)Redis實(shí)例是常見(jiàn)需求,但有時(shí)需要同時(shí)連接多個(gè)Redis實(shí)例(例如,主Redis用于業(yè)務(wù)數(shù)據(jù)存儲(chǔ),另一個(gè)Redis用于爬蟲(chóng)數(shù)據(jù)緩存)。本文將基于一個(gè)實(shí)際案例,詳細(xì)介紹如何在Spring Boot中優(yōu)雅地配置和使用多個(gè)Redis實(shí)例,解決常見(jiàn)的注入歧義問(wèn)題,并提供一個(gè)通用的Redis工具類(lèi)來(lái)簡(jiǎn)化操作。
背景
在一個(gè)Spring Boot項(xiàng)目中,我們需要連接兩臺(tái)Redis實(shí)例:
- 主Redis:用于常規(guī)業(yè)務(wù)數(shù)據(jù),配置在
spring.redis(數(shù)據(jù)庫(kù)索引4)。 - 爬蟲(chóng)Redis:用于爬蟲(chóng)相關(guān)數(shù)據(jù),配置在
spring.redis-crawler(數(shù)據(jù)庫(kù)索引7)。
項(xiàng)目中遇到以下問(wèn)題:
- 配置兩個(gè)
RedisTemplate時(shí),Spring容器找不到redisCrawlerConnectionFactory。 - 配置多個(gè)
RedisProperties導(dǎo)致注入歧義,拋出NoUniqueBeanDefinitionException。 - 需要一個(gè)通用的
RedisCache工具類(lèi),支持動(dòng)態(tài)選擇Redis實(shí)例,同時(shí)兼容現(xiàn)有代碼。
下面,我們將逐步解決這些問(wèn)題,并展示如何優(yōu)雅地實(shí)現(xiàn)多Redis連接。
項(xiàng)目配置
1. 配置文件(application.yml)
首先,在application.yml中定義兩套R(shí)edis配置:
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: -1msspring.redis:主Redis,數(shù)據(jù)庫(kù)索引4。spring.redis-crawler:爬蟲(chóng)Redis,數(shù)據(jù)庫(kù)索引7。- 如果Redis需要密碼,取消
password字段的注釋并設(shè)置正確值(此處已脫敏)。
2. 依賴(lài)配置(pom.xml)
確保項(xiàng)目包含以下依賴(lài):
<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客戶(hù)端。fastjson:用于自定義序列化(項(xiàng)目中使用了FastJson2JsonRedisSerializer)。
配置多個(gè)Redis實(shí)例
問(wèn)題1:找不到redisCrawlerConnectionFactory
最初,我們嘗試在RedisConfig.java中定義兩個(gè)RedisTemplate:
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 主Redis模板
}
@Bean(name = "redisTemplateCrawl")
public RedisTemplate<Object, Object> redisTemplateCrawl(@Qualifier("redisCrawlerConnectionFactory") RedisConnectionFactory redisCrawlerConnectionFactory) {
// 爬蟲(chóng)Redis模板
}啟動(dòng)時(shí)拋出異常:
No qualifying bean of type 'org.springframework.data.redis.connection.RedisConnectionFactory' available
原因:Spring Boot自動(dòng)為spring.redis創(chuàng)建了一個(gè)RedisConnectionFactory,但不會(huì)為spring.redis-crawler創(chuàng)建。redisTemplateCrawl依賴(lài)的redisCrawlerConnectionFactory未定義。
解決方法:顯式定義redisCrawlerConnectionFactory和對(duì)應(yīng)的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:根據(jù)redisCrawlerProperties創(chuàng)建連接工廠。
問(wèn)題2:RedisProperties注入歧義
配置了redisCrawlerProperties后,啟動(dòng)時(shí)又遇到新問(wèn)題:
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自動(dòng)為spring.redis創(chuàng)建了一個(gè)RedisProperties,而我們又定義了redisCrawlerProperties,導(dǎo)致LettuceConnectionConfiguration無(wú)法確定使用哪個(gè)RedisProperties。
解決方法:顯式定義主Redis的RedisProperties,并用@Primary標(biāo)記為默認(rèn):
@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標(biāo)記為默認(rèn)。redisConnectionFactory:為主Redis創(chuàng)建連接工廠,確保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);";
}
}實(shí)現(xiàn)通用的Redis工具類(lèi)
為了簡(jiǎn)化Redis操作,我們創(chuàng)建了一個(gè)RedisCache工具類(lèi),支持動(dòng)態(tài)選擇RedisTemplate,同時(shí)兼容現(xiàn)有代碼。
問(wèn)題3:動(dòng)態(tài)選擇Redis實(shí)例
最初的RedisCache.java只注入了一個(gè)RedisTemplate:
@Autowired public RedisTemplate redisTemplate;
這導(dǎo)致無(wú)法操作爬蟲(chóng)Redis。我們希望:
- 現(xiàn)有代碼繼續(xù)使用主Redis(
redisTemplate),無(wú)需修改。 - 新代碼可以通過(guò)參數(shù)選擇主Redis或爬蟲(chóng)Redis(
redisTemplateCrawl)。
解決方法:
- 注入兩個(gè)
RedisTemplate(redisTemplate和redisTemplateCrawl)。 - 保留原有方法,默認(rèn)使用
redisTemplate。 - 為每個(gè)方法添加帶
templateName參數(shù)的重load版本,支持選擇Redis實(shí)例。
最終的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);
}
// 其他方法類(lèi)似,省略完整代碼
}關(guān)鍵點(diǎn):
- 使用
@Qualifier注入redisTemplate和redisTemplateCrawl。 - 保留原有方法(如
setCacheObject(String key, T value)),默認(rèn)使用redisTemplate。 - 新增帶
templateName的重載方法(如setCacheObject(String templateName, String key, T value)),支持選擇Redis實(shí)例。 getRedisTemplate方法根據(jù)templateName返回對(duì)應(yīng)的RedisTemplate("crawl"返回redisTemplateCrawl,否則返回redisTemplate)。
使用示例
在Service層中:
@Service
public class MyService {
@Autowired
private RedisCache redisCache;
public void example() {
// 現(xiàn)有代碼:默認(rèn)使用主Redis (database: 4)
redisCache.setCacheObject("key1", "value1");
String value1 = redisCache.getCacheObject("key1");
// 新代碼:使用爬蟲(chóng)Redis (database: 7)
redisCache.setCacheObject("crawl", "key2", "value2");
String value2 = redisCache.getCacheObject("crawl", "key2");
}
}- 現(xiàn)有代碼無(wú)需修改,繼續(xù)使用主Redis。
- 新代碼通過(guò)
templateName="crawl"操作爬蟲(chóng)Redis。
優(yōu)化建議
使用枚舉替代字符串:
為避免templateName的硬編碼,可使用枚舉:
public enum RedisInstance {
DEFAULT,
CRAWL
}
private RedisTemplate getRedisTemplate(RedisInstance instance) {
return instance == RedisInstance.CRAWL ? redisTemplateCrawl : redisTemplate;
}使用示例:
redisCache.setCacheObject(RedisInstance.CRAWL, "key2", "value2");
錯(cuò)誤處理:
在getRedisTemplate中添加空檢查:
private RedisTemplate getRedisTemplate(String templateName) {
if (redisTemplate == null || redisTemplateCrawl == null) {
throw new IllegalStateException("RedisTemplate not initialized");
}
return "crawl".equalsIgnoreCase(templateName) ? redisTemplateCrawl : redisTemplate;
}連接測(cè)試:
確保Redis服務(wù)器可訪問(wèn)(此處IP已脫敏):
redis-cli -h [REDACTED_HOST] -p 6379 -n 4 # 主Redis redis-cli -h [REDACTED_HOST] -p 6379 -n 7 # 爬蟲(chóng)Redis
序列化器:
確保FastJson2JsonRedisSerializer實(shí)現(xiàn)正確,支持序列化和反序列化。
總結(jié)
通過(guò)以下步驟,我們?cè)赟pring Boot項(xiàng)目中實(shí)現(xiàn)了優(yōu)雅的多Redis連接:
- 在
application.yml中配置兩套R(shí)edis(spring.redis和spring.redis-crawler)。 - 在
RedisConfig.java中定義兩個(gè)RedisProperties、RedisConnectionFactory和RedisTemplate,使用@Primary和@Qualifier解決注入歧義。 - 實(shí)現(xiàn)
RedisCache工具類(lèi),支持動(dòng)態(tài)選擇Redis實(shí)例,同時(shí)兼容現(xiàn)有代碼。
這種方案既靈活又易于擴(kuò)展,適合需要操作多個(gè)Redis實(shí)例的場(chǎng)景。如果你有更多Redis實(shí)例,只需重復(fù)上述步驟,定義新的RedisProperties和RedisTemplate,并在RedisCache中擴(kuò)展支持。
參考:
- Spring Boot官方文檔:https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.redis
- Lettuce官方文檔:https://lettuce.io/
到此這篇關(guān)于在SpringBoot項(xiàng)目中優(yōu)雅的連接多臺(tái)Redis的操作方法的文章就介紹到這了,更多相關(guān)SpringBoot連接多臺(tái)Redis內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot連接Redis集群教程
- SpringBoot實(shí)現(xiàn)自定義Redis的連接的流程步驟
- 關(guān)于SpringBoot集成Lettuce連接Redis的方法和案例
- springboot連接不上redis的三種解決辦法
- springboot連接redis并動(dòng)態(tài)切換database的實(shí)現(xiàn)方法
- springboot 如何使用jedis連接Redis數(shù)據(jù)庫(kù)
- 關(guān)于Springboot2.x集成lettuce連接redis集群報(bào)超時(shí)異常Command timed out after 6 second(s)
- springboot連接Redis的教程詳解
- springboot2整合redis使用lettuce連接池的方法(解決lettuce連接池?zé)o效問(wèn)題)
相關(guān)文章
Mybatis之通用Mapper動(dòng)態(tài)表名及其原理分析
這篇文章主要介紹了Mybatis之通用Mapper動(dòng)態(tài)表名及其原理分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
java中for和forEach的速度比較實(shí)例Demo
for循環(huán)中的循環(huán)條件中的變量只求一次值,而foreach語(yǔ)句是java5新增,在遍歷數(shù)組、集合的時(shí)候,foreach擁有不錯(cuò)的性能,這篇文章主要給大家介紹了關(guān)于java中for和forEach速度比較的相關(guān)資料,需要的朋友可以參考下2021-08-08
Java List分頁(yè)功能實(shí)現(xiàn)代碼實(shí)例
這篇文章主要介紹了Java List分頁(yè)功能實(shí)現(xiàn)代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
Spring Data JPA實(shí)現(xiàn)排序與分頁(yè)查詢(xún)超詳細(xì)流程講解
在介紹Spring Data JPA的時(shí)候,我們首先認(rèn)識(shí)下Hibernate。Hibernate是數(shù)據(jù)訪問(wèn)解決技術(shù)的絕對(duì)霸主,使用O/R映射技術(shù)實(shí)現(xiàn)數(shù)據(jù)訪問(wèn),O/R映射即將領(lǐng)域模型類(lèi)和數(shù)據(jù)庫(kù)的表進(jìn)行映射,通過(guò)程序操作對(duì)象而實(shí)現(xiàn)表數(shù)據(jù)操作的能力,讓數(shù)據(jù)訪問(wèn)操作無(wú)須關(guān)注數(shù)據(jù)庫(kù)相關(guān)的技術(shù)2022-10-10
idea整合deepseek實(shí)現(xiàn)AI輔助編程的流程步驟
文章介紹了如何在IntelliJ IDEA中整合DeepSeek平臺(tái)實(shí)現(xiàn)AI輔助編程,步驟包括安裝CodeGPT插件、注冊(cè)DeepSeek開(kāi)發(fā)者賬號(hào)、配置API密鑰以及設(shè)置API信息,需要的朋友可以參考下2025-02-02
mybatis新增save結(jié)束后自動(dòng)返回主鍵id詳解
這篇文章主要介紹了mybatis新增save結(jié)束后自動(dòng)返回主鍵id詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
Java 調(diào)用 HTTP 接口的 7 種方式示例代碼(全網(wǎng)最全指南)
在開(kāi)發(fā)過(guò)程中,調(diào)用 HTTP 接口是最常見(jiàn)的需求之一,本文將詳細(xì)介紹 Java 中 7 種主流的調(diào)用 HTTP 接口的方式,包括每種工具的優(yōu)缺點(diǎn)和完整代碼實(shí)現(xiàn),感興趣的朋友一起看看吧2025-02-02
SpringBoot中全局異常處理的5種實(shí)現(xiàn)方式小結(jié)
在實(shí)際開(kāi)發(fā)中,異常處理是一個(gè)非常重要的環(huán)節(jié),合理的異常處理機(jī)制不僅能提高系統(tǒng)的健壯性,還能大大提升用戶(hù)體驗(yàn),下面我們就來(lái)看看SpringBoot中全局異常處理的5種實(shí)現(xiàn)方式吧2025-03-03
Java前后端分離的在線(xiàn)點(diǎn)餐系統(tǒng)實(shí)現(xiàn)詳解
這是一個(gè)基于SpringBoot+Vue框架開(kāi)發(fā)的在線(xiàn)點(diǎn)餐系統(tǒng)。首先,這是一個(gè)前后端分離的項(xiàng)目。具有一個(gè)在線(xiàn)點(diǎn)餐系統(tǒng)該有的所有功能,感興趣的朋友快來(lái)看看吧2022-01-01

