SpringBoot+Redis多DB動(dòng)態(tài)切換幾種方式總結(jié)
功能目標(biāo)
該方案實(shí)現(xiàn)了 Redis 多數(shù)據(jù)庫(多 DB index)環(huán)境下,通過注解或顯式指定的方式在代碼層自動(dòng)切換 Redis 數(shù)據(jù)庫,無需手動(dòng) select 操作,支持線程安全、可恢復(fù)的動(dòng)態(tài)切換機(jī)制。
動(dòng)態(tài)切換 Redis 庫的方案適合一些小型或簡單的應(yīng)用場景,主要用于 資源節(jié)約、簡單隔離、邏輯清晰 的情況。但隨著應(yīng)用的增長和流量的增加,使用 Redis 集群或其他分布式緩存方案可能會(huì)更加適應(yīng)需求。
模塊組成
| 組件 | 說明 |
| @SwitchDatabase | 注解:方法粒度聲明 Redis 使用哪個(gè)數(shù)據(jù)庫 |
| DatabaseSwitchAspect | AOP 切面:攔截 @SwitchDatabase 注解,自動(dòng)切庫 |
| DynamicDatabaseConnectionFactory | Redis 連接工廠擴(kuò)展:支持線程級(jí)別的 select(db) |
| RedisDatabaseSwitcher | 切庫控制器:封裝切換邏輯 |
| RedisUtil | Redis 工具類:封裝常用 Redis 操作,同時(shí)支持傳入 db 參數(shù)切庫 |
| RedisConfig | 配置類:初始化動(dòng)態(tài)連接工廠和 RedisTemplate |
使用方式
方式一:使用 @SwitchDatabase 注解自動(dòng)切庫(推薦)
適合無侵入的服務(wù)方法層使用,自動(dòng)切庫,自動(dòng)恢復(fù)。
@SwitchDatabase(1)
public void updateUserCache(String userId) {
redisUtil.set("user:" + userId, "someData");
}方式二:工具類中手動(dòng)指定 db 切換(適用于底層封裝或靈活需求)
redisUtil.set("shop:token:" + token, "abc", 60, TimeUnit.MINUTES, 2);
String val = redisUtil.get("shop:token:" + token, String.class, 2);自動(dòng)切換實(shí)現(xiàn)邏輯
1.@SwitchDatabase
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author chenxuefeng
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SwitchDatabase {
int value();// 目標(biāo)數(shù)據(jù)庫編號(hào)
}注解會(huì)被 AOP 切面攔截;
2.DatabaseSwitchAspect
import com.zrr.platform.reids.support.DynamicDatabaseConnectionFactory;
import com.zrr.platform.reids.support.RedisDatabaseSwitcher;
import com.zrr.platform.reids.annotation.SwitchDatabase;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 切面:攔截 @SwitchDatabase 注解的方法
* - 在方法執(zhí)行前切換數(shù)據(jù)庫
* - 執(zhí)行完成后恢復(fù)原數(shù)據(jù)庫
* @author zrr
*/
@Aspect
@Component
public class DatabaseSwitchAspect {
@Autowired
private RedisDatabaseSwitcher switcher;
@Around("@annotation(switchDatabase)")
public Object switchDatabase(ProceedingJoinPoint joinPoint, SwitchDatabase switchDatabase) throws Throwable {
// 記錄當(dāng)前數(shù)據(jù)庫
int originalDb = getCurrentDatabase();
try {
// 切換到目標(biāo)數(shù)據(jù)庫
switcher.switchDatabase(switchDatabase.value());
return joinPoint.proceed();
} finally {
// 恢復(fù)原來的數(shù)據(jù)庫
switcher.switchDatabase(originalDb);
}
}
private int getCurrentDatabase() {
// 從連接工廠獲取當(dāng)前數(shù)據(jù)庫編號(hào)(需擴(kuò)展 DynamicDatabaseConnectionFactory)
return ((DynamicDatabaseConnectionFactory) switcher.getConnectionFactory()).getCurrentDatabase();
}
}3.數(shù)據(jù)庫連接工廠 和 數(shù)據(jù)庫切換器
3.1 數(shù)據(jù)庫連接工廠
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
/**
* @author zrr
* 動(dòng)態(tài) Redis 數(shù)據(jù)庫連接工廠。
* 核心原理:
* - 使用 ThreadLocal 保存當(dāng)前線程應(yīng)使用的 Redis 數(shù)據(jù)庫編號(hào)(db index)
* - 重寫 getConnection() 方法,在每次獲取連接時(shí)調(diào)用 select(db),實(shí)現(xiàn)動(dòng)態(tài)切換
*/
public class DynamicDatabaseConnectionFactory extends LettuceConnectionFactory {
private final ThreadLocal<Integer> databaseHolder = ThreadLocal.withInitial(() -> 0);
public DynamicDatabaseConnectionFactory(RedisStandaloneConfiguration config,
LettucePoolingClientConfiguration clientConfig) {
super(config, clientConfig);
}
/**
* 設(shè)置當(dāng)前線程使用的數(shù)據(jù)庫編號(hào)
*/
public void setCurrentDatabase(int db) {
databaseHolder.set(db);
}
/**
* 獲取當(dāng)前線程設(shè)置的數(shù)據(jù)庫編號(hào)
*/
public int getCurrentDatabase() {
return databaseHolder.get();
}
/**
* 初始化后默認(rèn)執(zhí)行一次 select(db),確保連接池創(chuàng)建時(shí)使用默認(rèn) db
*/
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
// 初始化默認(rèn)數(shù)據(jù)庫
RedisConnection connection = getConnection();
connection.select(databaseHolder.get());
connection.close();
}
/**
* 每次從連接池中獲取連接時(shí)都執(zhí)行 select(db)
*/
@Override
public RedisConnection getConnection() {
RedisConnection connection = super.getConnection();
connection.select(databaseHolder.get());
return connection;
}
}3.2.數(shù)據(jù)庫切換器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Component;
/**
* @author zrr
* Redis 數(shù)據(jù)庫切換器
* 封裝對(duì) DynamicDatabaseConnectionFactory 的調(diào)用
*/
@Component
public class RedisDatabaseSwitcher {
@Autowired
private RedisConnectionFactory connectionFactory;
/**
* 切換 Redis 數(shù)據(jù)庫(當(dāng)前線程內(nèi)有效)
*/
public void switchDatabase(int db) {
if (connectionFactory instanceof DynamicDatabaseConnectionFactory) {
((DynamicDatabaseConnectionFactory) connectionFactory).setCurrentDatabase(db);
}
}
/**
* 獲取當(dāng)前的數(shù)據(jù)庫編號(hào)
*/
public int getCurrentDatabase() {
if (connectionFactory instanceof DynamicDatabaseConnectionFactory) {
return ((DynamicDatabaseConnectionFactory) connectionFactory).getCurrentDatabase();
}
throw new IllegalStateException("The RedisConnectionFactory is not of type DynamicDatabaseConnectionFactory");
}
public RedisConnectionFactory getConnectionFactory() {
return this.connectionFactory;
}
}記錄當(dāng)前線程數(shù)據(jù)庫 db,并切換至目標(biāo) db;
- 方法執(zhí)行完畢后自動(dòng)切換回原 db,避免影響線程后續(xù)邏輯;
- DynamicDatabaseConnectionFactory
重寫了 getConnection() 方法,確保每次連接池獲取連接時(shí)都執(zhí)行 select(db),真正實(shí)現(xiàn)動(dòng)態(tài)切庫;
4.RedisConfig配置
import com.zrr.platform.reids.support.DynamicDatabaseConnectionFactory;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author zrr
*/
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.database:0}")
private int defaultDatabase;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(host);
config.setPort(port);
config.setPassword(password);
// 默認(rèn)數(shù)據(jù)庫
config.setDatabase(defaultDatabase);
LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
.poolConfig(new GenericObjectPoolConfig())
.build();
DynamicDatabaseConnectionFactory factory = new DynamicDatabaseConnectionFactory(config, clientConfig);
//關(guān)鍵:關(guān)閉共享連接,支持 select(db)
factory.setShareNativeConnection(false);
return factory;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}5.RedisUtil工具類
import com.zrr.platform.reids.support.RedisDatabaseSwitcher;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author zrr
*/
@Component
@RequiredArgsConstructor
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisDatabaseSwitcher switcher;
// ================== String 操作 ==================
/**
* 設(shè)置緩存
*/
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 設(shè)置帶過期時(shí)間的緩存
*/
public void set(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
public void set(String key, Object value, long timeout, TimeUnit unit, int db) {
int originalDb = switcher.getCurrentDatabase();
try {
// 切換到目標(biāo)數(shù)據(jù)庫
switcher.switchDatabase(db);
// 設(shè)置值
set(key, value, timeout, unit);
} finally {
// 恢復(fù)原始數(shù)據(jù)庫
switcher.switchDatabase(originalDb);
}
}
/**
* 獲取緩存并轉(zhuǎn)換為指定類型
*/
@SuppressWarnings("unchecked")
public <T> T get(String key, Class<T> clazz) {
Object val = redisTemplate.opsForValue().get(key);
return (!clazz.isInstance(val)) ? null : (T) val;
}
/**
* 從指定 Redis 數(shù)據(jù)庫獲取緩存
*/
public <T> T get(String key, Class<T> clazz, int db) {
int original = switcher.getCurrentDatabase();
try {
switcher.switchDatabase(db);
return get(key, clazz);
} finally {
switcher.switchDatabase(original);
}
}
/**
* 刪除緩存
*/
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* 判斷緩存是否存在
*/
public boolean hasKey(String key) {
Boolean result = redisTemplate.hasKey(key);
return result != null && result;
}
// ================== Hash 操作 ==================
public void hSet(String key, String field, Object value) {
redisTemplate.opsForHash().put(key, field, value);
}
public Object hGet(String key, String field) {
return redisTemplate.opsForHash().get(key, field);
}
public void hDelete(String key, String field) {
redisTemplate.opsForHash().delete(key, field);
}
// ================== List 操作 ==================
public void lPush(String key, Object value) {
redisTemplate.opsForList().leftPush(key, value);
}
public Object lPop(String key) {
return redisTemplate.opsForList().leftPop(key);
}
public Object rPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
// ================== Set 操作 ==================
public void sAdd(String key, Object... values) {
redisTemplate.opsForSet().add(key, values);
}
public Set<Object> sMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
// ================== 分布式鎖 ==================
private static final String UNLOCK_LUA =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
/**
* 嘗試加鎖(默認(rèn) Redis DB)
*/
public boolean tryLock(String key, String uniqueId, long expireSeconds) {
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, uniqueId, Duration.ofSeconds(expireSeconds));
return Boolean.TRUE.equals(success);
}
/**
* 嘗試加鎖(指定 Redis DB)
*/
public boolean tryLock(String key, String uniqueId, long expireSeconds, int db) {
int originalDb = switcher.getCurrentDatabase();
try {
switcher.switchDatabase(db);
return tryLock(key, uniqueId, expireSeconds);
} finally {
switcher.switchDatabase(originalDb);
}
}
/**
* 解鎖(默認(rèn) Redis DB)
*/
public boolean unlock(String key, String uniqueId) {
Long result = redisTemplate.execute(
new DefaultRedisScript<>(UNLOCK_LUA, Long.class),
Collections.singletonList(key),
uniqueId
);
return result != null && result == 1L;
}
/**
* 解鎖(指定 Redis DB)
*/
public boolean unlock(String key, String uniqueId, int db) {
int originalDb = switcher.getCurrentDatabase();
try {
switcher.switchDatabase(db);
return unlock(key, uniqueId);
} finally {
switcher.switchDatabase(originalDb);
}
}
}線程安全說明
- 使用ThreadLocal 保存每個(gè)線程的 Redis DB index;
- 每次切換數(shù)據(jù)庫僅在當(dāng)前線程內(nèi)有效;
- 切面自動(dòng)恢復(fù)原數(shù)據(jù)庫,確保線程復(fù)用時(shí)數(shù)據(jù)一致性。
配置參考
application.yml:
spring:
redis:
host: localhost
port: 6379
password: your_password
database: 0 # 默認(rèn)數(shù)據(jù)庫,可通過注解/參數(shù)動(dòng)態(tài)切換示例:完整流程
import com.zll.platform.reids.annotation.SwitchDatabase;
import com.zll.platform.reids.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 緩存AOP切換庫測試類
* @author chenxuefeng
*/
@Component
public class RedisDemoService {
@Autowired
private RedisUtil redisUtil;
@SwitchDatabase(3)
public void testWithDb3() {
redisUtil.set("key-db3", "value3", 5, TimeUnit.MINUTES);
String value = redisUtil.get("key-db3", String.class);
System.out.println("從數(shù)據(jù)庫3中讀取的值: " + value);
}
@SwitchDatabase(4)
public void testWithDb4() {
redisUtil.set("key-db4", "value4", 5, TimeUnit.MINUTES);
String value = redisUtil.get("key-db4", String.class);
System.out.println("從數(shù)據(jù)庫4中讀取的值: " + value);
}
@SwitchDatabase(5)
public void testLocked() {
boolean locked = redisUtil.tryLock("lock:order", String.valueOf(123), 600);
String value = redisUtil.get("lock:order", String.class);
System.out.println("從數(shù)據(jù)庫5中讀取鎖的值: " + value);
}
}
//測試類
@SpringBootTest(classes = ZllPlatformApplication.class)
public class RedisUtilTest {
@Autowired
private RedisUtil redisUtil;
@Autowired
private RedisDemoService demoService;
@Autowired
private RedisDatabaseSwitcher switcher;
@Test
public void testLocked() {
// 簡單加鎖
boolean locked = redisUtil.tryLock("lock:order", String.valueOf(123), 600);
System.out.println(locked);
// 簡單釋放鎖
boolean unlocked = redisUtil.unlock("lock:order", String.valueOf(123));
System.out.println(unlocked);
}
@Test
public void testLocked5() {
demoService.testLocked();
}
@Test
public void testDb3() {
// 這里走的是代理,AOP 能生效
demoService.testWithDb3();
}
@Test
@SwitchDatabase(4)
public void testDb4() {
demoService.testWithDb4();
}
@Test
public void testStringSetAndGet5() {
switcher.switchDatabase(5); // 切到 db5
redisUtil.set("test", "test", 1, TimeUnit.HOURS);
String value = redisUtil.get("test", String.class);
System.out.println("Get from Redis (db5): " + value);
}
@Test
public void testMultiDbIsolation() {
redisUtil.set("key1", "val-db1", 10, TimeUnit.MINUTES, 1);
redisUtil.set("key1", "val-db2", 10, TimeUnit.MINUTES, 2);
String v1 = redisUtil.get("key1", String.class, 1);
String v2 = redisUtil.get("key1", String.class, 2);
System.out.println("db1 value: " + v1);
System.out.println("db2 value: " + v2);
}
@Test
public void testStringSetAndGet() {
redisUtil.set("test", "test", 1, TimeUnit.HOURS);
String value = redisUtil.get("test", String.class);
System.out.println("Get from Redis: " + value);
}
@Test
public void testList() {
redisUtil.lPush("myList", "v1");
Object val = redisUtil.lPop("myList");
System.out.println("Pop from List: " + val);
}
@Test
public void testSet() {
redisUtil.sAdd("mySet", "a", "b", "c");
Set<Object> members = redisUtil.sMembers("mySet");
System.out.println("Set Members: " + members);
}
@Test
public void testHash() {
redisUtil.hSet("myHash", "field1", "value1");
Object val = redisUtil.hGet("myHash", "field1");
System.out.println("Hash Value: " + val);
}后期擴(kuò)展建議
- ? 支持 Redisson 多數(shù)據(jù)源:與 RedisDatabaseSwitcher 一致管理 RedissonClient 切換;
- ?? 整合分布式鎖:Redisson 支持看門狗機(jī)制的鎖,配合 DB 切庫也能安全使用;
- ?? 支持緩存前綴隔離 + 庫隔離混合策略:既可多庫也能前綴隔離;
- ?? 全局默認(rèn) db 支持:如未指定注解或參數(shù)時(shí)自動(dòng) fallback 到配置文件默認(rèn) db。
總結(jié)對(duì)比表格
| 對(duì)比項(xiàng) | 自動(dòng)切庫方案(動(dòng)態(tài) DB) | 單庫方案(傳統(tǒng)) |
| 開發(fā)復(fù)雜度 | 高,需要自定義配置、切面 | 低,開箱即用 |
| 代碼簡潔性 | 高,一套工具支持多DB | 低,可能需要多個(gè) RedisTemplate |
| 多租戶支持 | 強(qiáng),天然適配 | 弱,需自定義邏輯 |
| 隔離性 | 好,不同庫物理隔離 | 差,需靠 key 前綴區(qū)分 |
| 性能 | 稍低,有 AOP 切面和上下文切換 | 高,無額外邏輯 |
| 可擴(kuò)展性 | 強(qiáng) | 弱 |
| 兼容 Redis Cluster | ? 不支持(PS:后期上云時(shí)記得選擇主從模式) | ? 支持 |
到此這篇關(guān)于SpringBoot+Redis多DB動(dòng)態(tài)切換幾種方式總結(jié)的文章就介紹到這了,更多相關(guān)SpringBoot Redis多DB動(dòng)態(tài)切換內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于Process的waitFor死鎖問題及解決方案
這篇文章主要介紹了關(guān)于Process的waitFor死鎖問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
Java Code Cache滿導(dǎo)致應(yīng)用性能降低問題解決
這篇文章主要介紹了Java Code Cache滿導(dǎo)致應(yīng)用性能降低問題解決,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
Spring 事務(wù)神器TransactionTemplate用法及避坑指南
TransactionTemplate是Spring框架提供的編程式事務(wù)管理模板類,它結(jié)合了聲明式事務(wù)和編程式事務(wù)的優(yōu)點(diǎn),解決了復(fù)雜場景下的事務(wù)控制問題,本文給大家介紹Spring 事務(wù)神器TransactionTemplate 全解析,感興趣的朋友跟隨小編一起看看吧2025-12-12
基于SpringBoot實(shí)現(xiàn)抽獎(jiǎng)活動(dòng)的四種策略
抽獎(jiǎng)活動(dòng)是產(chǎn)品運(yùn)營中常見的用戶激勵(lì)和互動(dòng)手段,通過隨機(jī)性和獎(jiǎng)勵(lì)刺激用戶參與度,提升用戶活躍度和留存率,在技術(shù)實(shí)現(xiàn)上,抽獎(jiǎng)系統(tǒng)涉及到隨機(jī)算法、獎(jiǎng)品分配、防作弊機(jī)制等多方面內(nèi)容,本文將介紹基于SpringBoot實(shí)現(xiàn)抽獎(jiǎng)活動(dòng)的5種策略,需要的朋友可以參考下2025-06-06

