java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot Redis多DB动态切换

SpringBoot+Redis多DB动态切换几种方式总结

作者:自燃人~

在现代Web应用开发中,缓存是提升系统性能的重要手段,Redis作为高性能的内存数据库,与SpringBoot的完美结合为我们提供了强大的缓存解决方案,这篇文章主要介绍了SpringBoot+Redis多DB动态切换几种方式的相关资料,需要的朋友可以参考下

功能目标

该方案实现了 Redis 多数据库(多 DB index)环境下,通过注解或显式指定的方式在代码层自动切换 Redis 数据库,无需手动 select 操作,支持线程安全、可恢复的动态切换机制。

动态切换 Redis 库的方案适合一些小型或简单的应用场景,主要用于 资源节约、简单隔离、逻辑清晰 的情况。但随着应用的增长和流量的增加,使用 Redis 集群或其他分布式缓存方案可能会更加适应需求。

模块组成

组件说明
@SwitchDatabase注解:方法粒度声明 Redis 使用哪个数据库
DatabaseSwitchAspect

AOP 切面:拦截

@SwitchDatabase

注解,自动切库

DynamicDatabaseConnectionFactoryRedis 连接工厂扩展:支持线程级别的 select(db)
RedisDatabaseSwitcher切库控制器:封装切换逻辑
RedisUtilRedis 工具类:封装常用 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;

重写了 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);
        }
    }
}

线程安全说明

配置参考

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);
    }

后期扩展建议

总结对比表格

对比项自动切库方案(动态 DB)单库方案(传统)
开发复杂度高,需要自定义配置、切面低,开箱即用
代码简洁性高,一套工具支持多DB低,可能需要多个 RedisTemplate
多租户支持强,天然适配弱,需自定义逻辑
隔离性好,不同库物理隔离差,需靠 key 前缀区分
性能稍低,有 AOP 切面和上下文切换高,无额外逻辑
可扩展性
兼容 Redis Cluster❌ 不支持(PS:后期上云时记得选择主从模式)✅ 支持

到此这篇关于SpringBoot+Redis多DB动态切换几种方式总结的文章就介绍到这了,更多相关SpringBoot Redis多DB动态切换内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文