SpringBoot+Redis多DB动态切换几种方式总结
作者:自燃人~
在现代Web应用开发中,缓存是提升系统性能的重要手段,Redis作为高性能的内存数据库,与SpringBoot的完美结合为我们提供了强大的缓存解决方案,这篇文章主要介绍了SpringBoot+Redis多DB动态切换几种方式的相关资料,需要的朋友可以参考下
功能目标
该方案实现了 Redis 多数据库(多 DB index)环境下,通过注解或显式指定的方式在代码层自动切换 Redis 数据库,无需手动 select 操作,支持线程安全、可恢复的动态切换机制。
动态切换 Redis 库的方案适合一些小型或简单的应用场景,主要用于 资源节约、简单隔离、逻辑清晰 的情况。但随着应用的增长和流量的增加,使用 Redis 集群或其他分布式缓存方案可能会更加适应需求。
模块组成
组件 | 说明 |
@SwitchDatabase | 注解:方法粒度声明 Redis 使用哪个数据库 |
DatabaseSwitchAspect | AOP 切面:拦截 @SwitchDatabase 注解,自动切库 |
DynamicDatabaseConnectionFactory | Redis 连接工厂扩展:支持线程级别的 select(db) |
RedisDatabaseSwitcher | 切库控制器:封装切换逻辑 |
RedisUtil | Redis 工具类:封装常用 Redis 操作,同时支持传入 db 参数切库 |
RedisConfig | 配置类:初始化动态连接工厂和 RedisTemplate |
使用方式
方式一:使用 @SwitchDatabase 注解自动切库(推荐)
适合无侵入的服务方法层使用,自动切库,自动恢复。
@SwitchDatabase(1) public void updateUserCache(String userId) { redisUtil.set("user:" + userId, "someData"); }
方式二:工具类中手动指定 db 切换(适用于底层封装或灵活需求)
redisUtil.set("shop:token:" + token, "abc", 60, TimeUnit.MINUTES, 2); String val = redisUtil.get("shop:token:" + token, String.class, 2);
自动切换实现逻辑
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();// 目标数据库编号 }
注解会被 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 注解的方法 * - 在方法执行前切换数据库 * - 执行完成后恢复原数据库 * @author zrr */ @Aspect @Component public class DatabaseSwitchAspect { @Autowired private RedisDatabaseSwitcher switcher; @Around("@annotation(switchDatabase)") public Object switchDatabase(ProceedingJoinPoint joinPoint, SwitchDatabase switchDatabase) throws Throwable { // 记录当前数据库 int originalDb = getCurrentDatabase(); try { // 切换到目标数据库 switcher.switchDatabase(switchDatabase.value()); return joinPoint.proceed(); } finally { // 恢复原来的数据库 switcher.switchDatabase(originalDb); } } private int getCurrentDatabase() { // 从连接工厂获取当前数据库编号(需扩展 DynamicDatabaseConnectionFactory) return ((DynamicDatabaseConnectionFactory) switcher.getConnectionFactory()).getCurrentDatabase(); } }
3.数据库连接工厂 和 数据库切换器
3.1 数据库连接工厂
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 * 动态 Redis 数据库连接工厂。 * 核心原理: * - 使用 ThreadLocal 保存当前线程应使用的 Redis 数据库编号(db index) * - 重写 getConnection() 方法,在每次获取连接时调用 select(db),实现动态切换 */ public class DynamicDatabaseConnectionFactory extends LettuceConnectionFactory { private final ThreadLocal<Integer> databaseHolder = ThreadLocal.withInitial(() -> 0); public DynamicDatabaseConnectionFactory(RedisStandaloneConfiguration config, LettucePoolingClientConfiguration clientConfig) { super(config, clientConfig); } /** * 设置当前线程使用的数据库编号 */ public void setCurrentDatabase(int db) { databaseHolder.set(db); } /** * 获取当前线程设置的数据库编号 */ public int getCurrentDatabase() { return databaseHolder.get(); } /** * 初始化后默认执行一次 select(db),确保连接池创建时使用默认 db */ @Override public void afterPropertiesSet() { super.afterPropertiesSet(); // 初始化默认数据库 RedisConnection connection = getConnection(); connection.select(databaseHolder.get()); connection.close(); } /** * 每次从连接池中获取连接时都执行 select(db) */ @Override public RedisConnection getConnection() { RedisConnection connection = super.getConnection(); connection.select(databaseHolder.get()); return connection; } }
3.2.数据库切换器
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.stereotype.Component; /** * @author zrr * Redis 数据库切换器 * 封装对 DynamicDatabaseConnectionFactory 的调用 */ @Component public class RedisDatabaseSwitcher { @Autowired private RedisConnectionFactory connectionFactory; /** * 切换 Redis 数据库(当前线程内有效) */ public void switchDatabase(int db) { if (connectionFactory instanceof DynamicDatabaseConnectionFactory) { ((DynamicDatabaseConnectionFactory) connectionFactory).setCurrentDatabase(db); } } /** * 获取当前的数据库编号 */ 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; } }
记录当前线程数据库 db,并切换至目标 db;
- 方法执行完毕后自动切换回原 db,避免影响线程后续逻辑;
- DynamicDatabaseConnectionFactory
重写了 getConnection() 方法,确保每次连接池获取连接时都执行 select(db),真正实现动态切库;
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); // 默认数据库 config.setDatabase(defaultDatabase); LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() .poolConfig(new GenericObjectPoolConfig()) .build(); DynamicDatabaseConnectionFactory factory = new DynamicDatabaseConnectionFactory(config, clientConfig); //关键:关闭共享连接,支持 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 操作 ================== /** * 设置缓存 */ public void set(String key, Object value) { redisTemplate.opsForValue().set(key, value); } /** * 设置带过期时间的缓存 */ 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 { // 切换到目标数据库 switcher.switchDatabase(db); // 设置值 set(key, value, timeout, unit); } finally { // 恢复原始数据库 switcher.switchDatabase(originalDb); } } /** * 获取缓存并转换为指定类型 */ @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 数据库获取缓存 */ 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"; /** * 尝试加锁(默认 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); } } /** * 解锁(默认 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 保存每个线程的 Redis DB index;
- 每次切换数据库仅在当前线程内有效;
- 切面自动恢复原数据库,确保线程复用时数据一致性。
配置参考
application.yml:
spring: redis: host: localhost port: 6379 password: your_password database: 0 # 默认数据库,可通过注解/参数动态切换
示例:完整流程
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("从数据库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("从数据库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("从数据库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); }
后期扩展建议
- ✅ 支持 Redisson 多数据源:与 RedisDatabaseSwitcher 一致管理 RedissonClient 切换;
- 🔒 整合分布式锁:Redisson 支持看门狗机制的锁,配合 DB 切库也能安全使用;
- 🚀 支持缓存前缀隔离 + 库隔离混合策略:既可多库也能前缀隔离;
- 👀 全局默认 db 支持:如未指定注解或参数时自动 fallback 到配置文件默认 db。
总结对比表格
对比项 | 自动切库方案(动态 DB) | 单库方案(传统) |
开发复杂度 | 高,需要自定义配置、切面 | 低,开箱即用 |
代码简洁性 | 高,一套工具支持多DB | 低,可能需要多个 RedisTemplate |
多租户支持 | 强,天然适配 | 弱,需自定义逻辑 |
隔离性 | 好,不同库物理隔离 | 差,需靠 key 前缀区分 |
性能 | 稍低,有 AOP 切面和上下文切换 | 高,无额外逻辑 |
可扩展性 | 强 | 弱 |
兼容 Redis Cluster | ❌ 不支持(PS:后期上云时记得选择主从模式) | ✅ 支持 |
到此这篇关于SpringBoot+Redis多DB动态切换几种方式总结的文章就介绍到这了,更多相关SpringBoot Redis多DB动态切换内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!