1. 程式人生 > 資料庫 >Redis-動態切換資料庫(整合SpringBoot工程)

Redis-動態切換資料庫(整合SpringBoot工程)

文章目錄

目標

  • 瞭解動態切換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中指定。

在Redis 提供的連線方式,選擇資料庫必須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個連線。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-RYBPYXmW-1609897634990)(index_files/1609854319439.png)]

第二種

瞭解到第一種方式的問題,目的在於解決上述問題。考慮到使用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提供的註解和介面,有必要了解一下,平時有些註解和介面是不怎麼用到的。