文章目录

  • 目标
  • 前言
  • 动态切换数据库
  • 思路
  • 第一种:
  • 第二种
  • 代码实现
  • 构建多个RedisTemplate
  • yml 配置
  • 初始化
  • 测试方法
  • 启动日志
  • 注意

目标

  • 了解动态切换Redis数据库
  • 了解Spring提供的一些注解和接口

前言

在某些场景下,不同业务服务使用同一个Redis数据库,为了以便于得到相对独立的数据库,需要一个服务对应一个数据库,在Redis中默认提供了16个数据库(序号0-15),那么就需要动态的切换数据库。

切换数据库命令,只需要选择序号即可。

127.0.0.1:0>select 1
"OK"

默认的数据库数量可以通过修改Redis配置文件修改

# 设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
databases 16

注意在集群模式下,不支持使用select命令来切换db,因为Redis集群模式下只有一个db0

动态切换数据库

以下内容基于 Spring boot 2.2.7.RELEASE、 Spring data Redis 、 Redisson框架 环境下。

思路

第一种:

最简单的想法,每次需要切换数据库时,就执行切换数据库命令,拿到切换后的Rredis连接,再操作Redis数据库即可。(先说明这个方式有很大问题,会有线程并发安全等问题,不过建议了解一下)

映射到代码中 RedisTemplate并没有提供执行切换数据库的方法,只能在RedisConnectionFactory中指定。

在Redisson 提供的连接方式,选择数据库必须Config对象中指定,才能生效。

代码示例:

// -- RedissonClient 是Redisson 框架提供的
 
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 切换数据库
     *
     * @return
     */
    public RedisTemplate switchDatabase(Integer dbIndex) {
        logger.info("重新建立redis数据库连接开始");
        // 配置类
        Config config = new Config();
        config.setCodec(StringCodec.INSTANCE);
        SingleServerConfig singleConfig = config.useSingleServer();
        singleConfig.setAddress("redis://127.0.0.1:6379");
        singleConfig.setPassword("***");
        // 指定连接的数据库
        singleConfig.setDatabase(dbIndex);
        RedissonClient redissonClient = Redisson.create(config);
        RedissonConnectionFactory redisConnectionFactory = new RedissonConnectionFactory(redissonClient);
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        logger.info("重新建立redis数据库连接结束");
        return redisTemplate;
    }

测试代码:

/**
     * 动态切换数据库,并设置数据
     * @param code 键
     * @param dbindex Redis 数据库序号 
     */
public ReturnData dynamicDatabaseSelection(@RequestParam("code") String code, @RequestParam("dbindex") Integer dbindex) {
        Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<>();
        DefaultTypedTuple typedTuple = new DefaultTypedTuple("zhangsan", 88D);
        DefaultTypedTuple typedTuple1 = new DefaultTypedTuple("zhangsan", 77D);
        DefaultTypedTuple typedTuple2 = new DefaultTypedTuple("lisi", 68D);
        DefaultTypedTuple typedTuple3 = new DefaultTypedTuple("wangwu", 120D);
        tuples.add(typedTuple);
        tuples.add(typedTuple1);
        tuples.add(typedTuple2);
        tuples.add(typedTuple3);
        redisUtil.switchDatabase(dbindex);
//        try {
//            logger.info("当前业务执行业务开始");
//            Thread.sleep(30000);
//            logger.info("耗时结束");
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
        // 执行切换数据库
        redisUtil.zsSetAndTime(code, tuples);
        // 设置数据
        Set<ZSetOperations.TypedTuple<Object>> set = redisUtil.zsGetReverseWithScores(code, 0, -1);
        ReturnData ret = ReturnData.newInstance();
        ret.setSuccess();
        ret.setMessage(set);
        return ret;
    }

正常情况下, 执行上述代码会正常切换到对应数据库,并设置数据。那么采用这种方式,就可以解决动态切换数据库的问题了吗?会有以下问题

  • 如果有多个请求同时进入,当第一个请求切换数据库触发,另一个请求同时切换数据库。共用了同一个RedisTemplate会导致第一个请求的设置的数据,可能被写入到第二个请求切换的数据库中,会出现线程并发问题。(可以把线程休眠的代码放开,测试
  • 程序每次切换数据库,都要重新与Redis服务器建立连接,耗时长(如下连接大约耗时1秒,如果多个请求同时切换,连接速度将更加缓慢)。
// 连接日志
2021-01-05 21:28:46.105 INFO  http-nio-8089-exec-10 fun.gengzi.codecopy.dao.RedisUtil Line:928 - 重新建立redis数据库连接开始
2021-01-05 21:28:46.316 INFO  http-nio-8089-exec-10 org.redisson.Version Line:41  - Redisson 3.12.0
2021-01-05 21:28:46.590 INFO  redisson-netty-22-18 org.redisson.connection.pool.MasterPubSubConnectionPool Line:168 - 1 connections initialized for 127.0.0.1/127.0.0.1:6379
2021-01-05 21:28:47.560 INFO  redisson-netty-22-19 org.redisson.connection.pool.MasterConnectionPool Line:168 - 24 connections initialized for 127.0.0.1/127.0.0.1:6379
2021-01-05 21:28:47.563 INFO  http-nio-8089-exec-10 fun.gengzi.codecopy.dao.RedisUtil Line:938 - 重新建立redis数据库连接结束
  • 当切换多次,会重复创建多个Rediscliet ,浪费资源。当系统存在多个 RedisClient 势必要占用内存和线程数 ,并对Redis服务器保持链接,占用服务器资源。
# info 查看Redis服务器信息
# info clients 已连接客户端信息,包含以下域:   
    # connected_clients : 已连接客户端的数量(不包括通过从属服务器连接的客户端)
    # client_longest_output_list : 当前连接的客户端当中,最长的输出列表
    # client_longest_input_buf : 当前连接的客户端当中,最大输入缓存
    # blocked_clients : 正在等待阻塞命令(BLPOP、BRPOP、BRPOPLPUSH)的客户端的数量
127.0.0.1:0>info clients
"# Clients
connected_clients:303
client_recent_max_input_buffer:2
client_recent_max_output_buffer:0
blocked_clients:0
 
# 查询内存占用
127.0.0.1:0>info memory

使用Redis Desktop Manager 工具,也可以查看。每切换一次数据库,增加25个连接。

redis command 动态库 redis动态切换数据源_数据库

第二种

了解到第一种方式的问题,目的在于解决上述问题。考虑到使用Redis 数据库个数是有限的,为每一个Redis 数据库创建一个连接池,不用每次切换都重新创建,复用之前的连接即可。上述有请求并发安全问题,最好是每一个数据库建立一个RedisTemplate,使用Map<String, RedisTemplate>来存储RedisTemplate,需要哪个RedisTemplate,就根据数据库序号获取RedisTemplate进行操作数据库。

那么在项目初始化时,就将需要使用的数据库连接建好是不错的选择。

代码实现

环境: Redis 5.0.8 版本、Redis Desktop Manager 工具

开发环境: Spring boot 2.2.7.RELEASE、 Spring data Redis 、 Redisson框架

构建多个RedisTemplate

yml 配置

在application.yml 加入以下自定义配置

redissondb:
  address: "redis://127.0.0.1:6379"
  password: 111
  # 数据库序号集合
  databases: [2,3,4,5,6]

初始化

读取配置初始化 RedisTemplate Bean

初始化类,在执行前会先执行RedisRegister 类,然后从Spring容器中获取redisTemplate ,将其设置到Map中。

/**
* <h1>RedisTemplate 初始化类 </h1>
*
* @author gengzi
* @date 2020年12月16日22:38:46
*/
@AutoConfigureBefore({RedisAutoConfiguration.class})  // 要在RedisAutoConfiguration 自动配置前执行
@Import(RedisRegister.class) // 配置该类前,先加载 RedisRegister 类
@Configuration // 配置类
// 实现 EnvironmentAware 用于获取全局环境
// 实现 ApplicationContextAware 用于获取Spring Context 上下文
public class RedisBeanInit implements EnvironmentAware, ApplicationContextAware {
    private Logger logger = LoggerFactory.getLogger(RedisBeanInit.class);
 
    // 用于获取环境配置
    private Environment environment;
    // 用于绑定对象
    private Binder binder;
    // Spring context
    private ApplicationContext applicationContext;
    // 线程安全的hashmap
    private Map<String, RedisTemplate> redisTemplateMap = new ConcurrentHashMap<>();
 
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    /**
     * 设置环境
     *
     * @param environment
     */
    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
        this.binder = Binder.get(environment);
    }
 
 
    @PostConstruct // Constructor >> @Autowired >> @PostConstruct 用于执行一个 非静态的void 方法,常应用于初始化资源
    public void initAllRedisTemlate() {
        logger.info("<<<初始化系统的RedisTemlate开始>>>");
        RedissondbConfigEntity redissondb;
        try {
            redissondb = binder.bind("redissondb", RedissondbConfigEntity.class).get();
        } catch (Exception e) {
            logger.error("读取redissondb环境配置失败", e);
            return;
        }
        List<Integer> databases = redissondb.getDatabases();
        if (CollectionUtils.isNotEmpty(databases)) {
            databases.forEach(db -> {
                Object bean = applicationContext.getBean("redisTemplate" + db);
                if (bean != null && bean instanceof RedisTemplate) {
                    redisTemplateMap.put("redisTemplate" + db, (RedisTemplate) bean);
                } else {
                    throw new RrException("初始化RedisTemplate" + db + "失败,请检查配置");
                }
            });
        }
        logger.info("已经装配的redistempleate,map:{}", redisTemplateMap);
        logger.info("<<<初始化系统的RedisTemlate完毕>>>");
    }
 
    @Bean
    public RedisManager getRedisManager() {
        return new RedisManager(redisTemplateMap);
    }
}

用于根据数据库序号获取对应的RedisTemplate

/**
* <h1>redis管理</h1>
*
* @author gengzi
* @date 2020年12月16日22:37:18
*/
public class RedisManager {
 
    private Map<String, RedisTemplate> redisTemplateMap = new ConcurrentHashMap<>();
 
    /**
     * 构造方法初始化 redisTemplateMap 的数据
     *
     * @param redisTemplateMap
     */
    public RedisManager(Map<String, RedisTemplate> redisTemplateMap) {
        this.redisTemplateMap = redisTemplateMap;
    }
 
    /**
     * 根据数据库序号,返回对应的RedisTemplate
     *
     * @param dbIndex 序号
     * @return {@link RedisTemplate}
     */
    public RedisTemplate getRedisTemplate(Integer dbIndex) {
        RedisTemplate redisTemplate = redisTemplateMap.get("redisTemplate" + dbIndex);
        if (redisTemplate == null) {
            throw new RrException("Map不存在该redisTemplate");
        }
        return redisTemplate;
    }
 
}

RedisTemplate Bean 初始化类,用于读取yml配置,创建多个RedisTemplate Bean 并注册到Spring容器。

/**
* <h1>redistemplate初始化</h1>
* <p>
* 作用:
* <p>
* 读取系统配置,系统启动时,读取redis 的配置,初始化所有的redistemplate
* 并动态注册为bean
*
* @author gengzi
* @date 2021年1月5日22:16:29
*/
@Configuration
// 实现 EnvironmentAware 用于获取环境配置
// 实现 ImportBeanDefinitionRegistrar 用于动态注册bean
public class RedisRegister implements EnvironmentAware, ImportBeanDefinitionRegistrar {
 
    private Logger logger = LoggerFactory.getLogger(RedisRegister.class);
 
    // 用于获取环境配置
    private Environment environment;
    // 用于绑定对象
    private Binder binder;
 
    /**
     * 设置环境
     *
     * @param environment
     */
    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
        this.binder = Binder.get(environment);
    }
 
    /**
     * 注册bean
     *
     * @param importingClassMetadata
     * @param registry
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        logger.info("《《《动态注册bean开始》》》");
        RedissondbConfigEntity redissondb;
        try {
            redissondb = binder.bind("redissondb", RedissondbConfigEntity.class).get();
        } catch (Exception e) {
            logger.error("读取redissondb环境配置失败", e);
            return;
        }
        List<Integer> databases = redissondb.getDatabases();
        if (CollectionUtils.isNotEmpty(databases)) {
            databases.forEach(db -> {
                // 单机模式,集群只能使用db0
                Config config = new Config();
                config.setCodec(StringCodec.INSTANCE);
                SingleServerConfig singleConfig = config.useSingleServer();
                singleConfig.setAddress(redissondb.getAddress());
                singleConfig.setPassword(redissondb.getPassword());
                singleConfig.setDatabase(db);
                RedissonClient redissonClient = Redisson.create(config);
                // 构造RedissonConnectionFactory
                RedissonConnectionFactory redisConnectionFactory = new RedissonConnectionFactory(redissonClient);
                // bean定义
                GenericBeanDefinition redisTemplate = new GenericBeanDefinition();
                // 设置bean 的类型
                redisTemplate.setBeanClass(RedisTemplate.class);
                // 设置自动注入的形式,根据名称
                redisTemplate.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME);
                // redisTemplate 的属性配置
                redisTemplate(redisTemplate, redisConnectionFactory);
                // 注册Bean
                registry.registerBeanDefinition("redisTemplate" + db, redisTemplate);
            });
        }
        logger.info("《《《动态注册bean结束》》》");
 
    }
 
    /**
     * redisTemplate 的属性配置
     *
     * @param redisTemplate          泛型bean
     * @param redisConnectionFactory 连接工厂
     * @return
     */
    public GenericBeanDefinition redisTemplate(GenericBeanDefinition redisTemplate, RedisConnectionFactory redisConnectionFactory) {
        RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // key采用String的序列化方式,value采用json序列化方式
        // 通过方法设置属性值
        redisTemplate.getPropertyValues().add("connectionFactory", redisConnectionFactory);
        redisTemplate.getPropertyValues().add("keySerializer", stringRedisSerializer);
        redisTemplate.getPropertyValues().add("hashKeySerializer", stringRedisSerializer);
        redisTemplate.getPropertyValues().add("valueSerializer", jackson2JsonRedisSerializer);
        redisTemplate.getPropertyValues().add("hashValueSerializer", jackson2JsonRedisSerializer);
        return redisTemplate;
    }
}

要点:根据序号集合,使用GenericBeanDefinition循环创建redisTemplate多个bean,再使用BeanDefinitionRegistry 将这些Bean注册到Spring容器,再根据序号,将其加入到Map中。 这里使用了Binder 将自定义配置映射成为 RedissondbConfigEntity 如下:

yml配置对象属性映射类

/**
* <h1>redisconfig 配置实体类</h1>
*
* @author gengzi
* @date 2020年12月16日14:09:41
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RedissondbConfigEntity implements Serializable {
    // 地址
    private String address;
    // 密码
    private String password;
    // 所有的db序号
    private List<Integer> databases = new ArrayList<>();
}

####工具方法

@Autowired
    private RedisManager redisManager;
   /**
     * zset 添加元素
     *
     * @param key
     * @param tuples
     * @return
     */
    public long zsSetAndTime(String key, Set<ZSetOperations.TypedTuple<Object>> tuples, Integer db) {
        try {
            // 根据db序号,获取对应的 RedisTemplate
            RedisTemplate redisTemplate = redisManager.getRedisTemplate(db);
            Long count = redisTemplate.opsForZSet().add(key, tuples);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

测试方法

@ApiOperation(value = "redis 动态选择数据库 新方式", notes = "redis 动态选择数据库 新方式")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "code", value = "code", required = true),
            @ApiImplicitParam(name = "dbindex", value = "dbindex", required = true)})
    @PostMapping("/dynamicDatabaseSelectionNew")
    @ResponseBody
    public ReturnData dynamicDatabaseSelectionNew(@RequestParam("code") String code, @RequestParam("dbindex") Integer dbindex) {
        Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<>();
        DefaultTypedTuple typedTuple = new DefaultTypedTuple("zhangsan", 88D);
        DefaultTypedTuple typedTuple1 = new DefaultTypedTuple("zhangsan", 77D);
        DefaultTypedTuple typedTuple2 = new DefaultTypedTuple("lisi", 68D);
        DefaultTypedTuple typedTuple3 = new DefaultTypedTuple("wangwu", 120D);
        tuples.add(typedTuple);
        tuples.add(typedTuple1);
        tuples.add(typedTuple2);
        tuples.add(typedTuple3);
        // 选择数据库,并设置数据
        redisUtil.zsSetAndTime(code, tuples, dbindex);
        Set<ZSetOperations.TypedTuple<Object>> set = redisUtil.zsGetReverseWithScores(code, 0, -1, dbindex);
        ReturnData ret = ReturnData.newInstance();
        ret.setSuccess();
        ret.setMessage(set);
        return ret;
    }

启动日志

2021-01-05 22:38:54.274 INFO  main fun.gengzi.codecopy.business.redis.config.RedisRegister Line:89  - 《《《动态注册bean开始》》》
2021-01-05 22:38:54.727 INFO  main org.redisson.Version Line:41  - Redisson 3.12.0
2021-01-05 22:38:57.326 INFO  redisson-netty-2-19 org.redisson.connection.pool.MasterPubSubConnectionPool Line:168 - 1 connections initialized for 127.0.0.1/127.0.0.1:6379
2021-01-05 22:38:57.325 INFO  redisson-netty-2-18 org.redisson.connection.pool.MasterConnectionPool Line:168 - 24 connections initialized for 127.0.0.1/127.0.0.1:6379
// -- 忽略中间连接Redis数据库日志
2021-01-05 22:39:01.385 INFO  main fun.gengzi.codecopy.business.redis.config.RedisRegister Line:122 - 《《《动态注册bean结束》》》
 
2021-01-05 22:39:13.307 INFO  main fun.gengzi.codecopy.business.redis.config.RedisBeanInit Line:68  - <<<初始化系统的RedisTemlate开始>>>
2021-01-05 22:39:13.682 INFO  main fun.gengzi.codecopy.business.redis.config.RedisBeanInit Line:87  - 已经装配的redistempleate,map:{redisTemplate6=org.springframework.data.redis.core.RedisTemplate@60e5d4fb, redisTemplate5=org.springframework.data.redis.core.RedisTemplate@19b8bcb5, redisTemplate2=org.springframework.data.redis.core.RedisTemplate@36d9efa6, redisTemplate4=org.springframework.data.redis.core.RedisTemplate@f11fad9, redisTemplate3=org.springframework.data.redis.core.RedisTemplate@68812b74}
2021-01-05 22:39:13.683 INFO  main fun.gengzi.codecopy.business.redis.config.RedisBeanInit Line:88  - <<<初始化系统的RedisTemlate完毕>>>

当在多次切换数据库,不会增加数据库连接,也不会出现请求并发问题。

可以观察 Redis Desktop Manager 服务器信息的变化,看是否达到预期。

注意

其中使用了不少Spring提供的注解和接口,有必要了解一下,平时有些注解和接口是不怎么用到的。