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