Springboot動態redis 資料來源資料庫

語言: CN / TW / HK

“我正在參加「掘金·啟航計劃」” 在日常的開發過程中我們都使用過redis 做快取什麼的。 基本上都是使用官方的data-redis  來進行整合使用。 但是官方的只能支援單資料來源的, 不支援多資料來源的。 要是配置多資料來源的情況下, 還要配置多個redisConnectionfactory , 配置多個redistemplate  同樣的程式碼要寫多份。這個很不友好,最近在想,能不能搞一個starts 封裝一下。類似mybatis-plus 團隊的動態資料來源一樣是基於註解和配置檔案的。 我在網上找了很多資料,大部分都是怎麼切換redis 資料庫的, 沒有切換redis資料來源的。最後在知乎上面找到老哥的這篇文章, https://zhuanlan.zhihu.com/p/405242915 (如有侵權,請聯絡刪除)。給了我新思路的大門。下面我們就來自己搞一個基於配置檔案和註解的redis 動態資料來源和動態資料庫的切換。

1, 準備工作,新建一個一個springboot maven 工程, 這裡我們不需要web的依賴,只需要data-redis 的依賴就行的。

2, 程式碼邏輯

3, 正式的寫程式碼

大部分的程式碼都和之前那個老哥文章程式碼差不多, 這裡我只是加上了切換redis 資料庫的邏輯。 核心程式碼 

``` package com.ducheng.multi.redis; import org.springframework.beans.factory.DisposableBean;import org.springframework.beans.factory.InitializingBean;import org.springframework.dao.DataAccessException;import org.springframework.data.redis.connection.;import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;import org.springframework.util.ObjectUtils;import java.util.Map; public class MultiRedisConnectionFactory implements InitializingBean, DisposableBean, RedisConnectionFactory, ReactiveRedisConnectionFactory { private final Map connectionFactoryMap; /
*
當前redis的名字
* /
private static final ThreadLocal currentRedisName = new ThreadLocal<>(); /

* 當前redis的db資料庫
/
private static final ThreadLocal currentRedisDb = new ThreadLocal<>();

public MultiRedisConnectionFactory(Map<String, LettuceConnectionFactory> connectionFactoryMap) {       
this.connectionFactoryMap = connectionFactoryMap;   
}
public void setCurrentRedis(String currentRedisName) {       
if (!connectionFactoryMap.containsKey(currentRedisName)) { 
      throw new RuntimeException("invalid currentRedis: " + currentRedisName + ",  it does not exists in configuration");       
}       
MultiRedisConnectionFactory.currentRedisName.set(currentRedisName);   
}


/**     * 選擇連線和資料庫    
* @param currentRedisName    
* @param db     */    
public void setCurrentRedis(String currentRedisName,Integer db) {        
if (!connectionFactoryMap.containsKey(currentRedisName)) {            
throw new RuntimeException("invalid currentRedis: " + currentRedisName + ",  it does not exists in configuration");       
}        
MultiRedisConnectionFactory.currentRedisName.set(currentRedisName);         MultiRedisConnectionFactory.currentRedisDb.set(db);    
}

@Override    
public void destroy() throws Exception {
connectionFactoryMap.values().forEach(LettuceConnectionFactory::destroy);        }
@Override    
public void afterPropertiesSet() throws Exception {  
connectionFactoryMap.values().forEach(LettuceConnectionFactory::afterPropertiesSet);   
}

private LettuceConnectionFactory currentLettuceConnectionFactory() { 
String currentRedisName = MultiRedisConnectionFactory.currentRedisName.get();       
       if (!ObjectUtils.isEmpty(currentRedisName)) {  
       MultiRedisConnectionFactory.currentRedisName.remove();           
       return connectionFactoryMap.get(currentRedisName);       
       }      
     return connectionFactoryMap.get(MultiRedisProperties.DEFAULT);    
}

@Override   
public ReactiveRedisConnection getReactiveConnection() {

return currentLettuceConnectionFactory().getReactiveConnection();

}


@Override    
public ReactiveRedisClusterConnection getReactiveClusterConnection() {        
return currentLettuceConnectionFactory().getReactiveClusterConnection();

}
@Override

public RedisConnection getConnection() {   
// 這裡就是切換資料庫的地方        
Integer currentRedisDb = MultiRedisConnectionFactory.currentRedisDb.get();       
if (!ObjectUtils.isEmpty(currentRedisDb)) {

LettuceConnectionFactory lettuceConnectionFactory =   currentLettuceConnectionFactory();

lettuceConnectionFactory.setShareNativeConnection(false);

RedisConnection connection = lettuceConnectionFactory.getConnection();

connection.select(currentRedisDb);

return connection;       
}       
return   currentLettuceConnectionFactory().getConnection();    
}
@Override    
public RedisClusterConnection getClusterConnection() {

return currentLettuceConnectionFactory().getClusterConnection();

}

@Override    
public boolean getConvertPipelineAndTxResults() {

return currentLettuceConnectionFactory().getConvertPipelineAndTxResults();    }
@Override    
public RedisSentinelConnection getSentinelConnection() {

return currentLettuceConnectionFactory().getSentinelConnection();    
}

@Override    
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {        
return currentLettuceConnectionFactory().translateExceptionIfPossible(ex);    
}

} ``` 根據條件註解注入redis 資料庫的工廠 核心程式碼 

``` package org.springframework.boot.autoconfigure.data.redis;

import com.ducheng.multi.redis.MultiRedisConnectionFactory;import com.ducheng.multi.redis.MultiRedisProperties;import io.lettuce.core.resource.ClientResources;import org.springframework.beans.factory.ObjectProvider;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisClusterConfiguration;import org.springframework.data.redis.connection.RedisSentinelConfiguration;import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import java.util.HashMap;import java.util.Map; @ConditionalOnProperty(prefix = "spring.redis", value = "enable-multi", matchIfMissing = false)@Configuration(proxyBeanMethods = false)public class RedisCustomizedConfiguration { /* * @param builderCustomizers
* @param clientResources
* @param multiRedisProperties
* @return * @see org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration
/
@Bean
public MultiRedisConnectionFactory multiRedisConnectionFactory( ObjectProvider builderCustomizers, ClientResources clientResources,
MultiRedisProperties multiRedisProperties,
ObjectProvider sentinelConfigurationProvider, ObjectProvider clusterConfigurationProvider) { Map connectionFactoryMap = new HashMap<>();

Map<String, RedisProperties> multi = multiRedisProperties.getMulti();  
multi.forEach((k, v) -> {            
LettuceConnectionConfiguration lettuceConnectionConfiguration = new LettuceConnectionConfiguration(                    v,                    sentinelConfigurationProvider,                    clusterConfigurationProvider            );           
LettuceConnectionFactory lettuceConnectionFactory = lettuceConnectionConfiguration.redisConnectionFactory(builderCustomizers, clientResources);           
connectionFactoryMap.put(k, lettuceConnectionFactory);       
});        
return new MultiRedisConnectionFactory(connectionFactoryMap);   
}

} ```

redis 的配置類

``` @ConfigurationProperties(prefix = "spring.redis")public class MultiRedisProperties {
/* * 預設連線必須配置,配置 key 為 default /
public static final String DEFAULT = "default"; private boolean enableMulti = false;

private Map<String, RedisProperties> multi;
public boolean isEnableMulti() {       
return enableMulti;    
}
public void setEnableMulti(boolean enableMulti) {        
this.enableMulti = enableMulti;    
}
public Map<String, RedisProperties> getMulti() {       
return multi;    
}
public void setMulti(Map<String, RedisProperties> multi) {        
this.multi = multi;    
}
public MultiRedisProperties() {}
}

```

程式碼測試:

配置檔案配置多資料來源:

spring.redis.enable-multi=true spring.redis.multi.default.host=xxxxxxxx spring.redis.multi.default.port=6381 spring.redis.multi.test.host=xxxxxxxx spring.redis.multi.test.port=6380

配置redisTemplate 

@Bean public RedisTemplate<String, Object> template(RedisConnectionFactory factory) { // 建立RedisTemplate<String, Object>物件 RedisTemplate<String, Object> template = new RedisTemplate<>(); // 配置連線工廠 template.setConnectionFactory(factory); // 定義Jackson2JsonRedisSerializer序列化物件 Jackson2JsonRedisSerializer<Object> jacksonSeial = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); // 指定要序列化的域,field,get和set,以及修飾符範圍,ANY是都有包括private和public om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 指定序列化輸入的型別,類必須是非final修飾的,final修飾的類,比如String,Integer等會報異常 om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jacksonSeial.setObjectMapper(om); StringRedisSerializer stringSerial = new StringRedisSerializer(); // redis key 序列化方式使用stringSerial template.setKeySerializer(stringSerial); // redis value 序列化方式使用jackson template.setValueSerializer(jacksonSeial); // redis hash key 序列化方式使用stringSerial template.setHashKeySerializer(stringSerial); // redis hash value 序列化方式使用jackson template.setHashValueSerializer(jacksonSeial); template.afterPropertiesSet(); return template; }

編寫測試程式碼: ``` package com.ducheng.multi.redis; import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.data.redis.connection.RedisConnection;import org.springframework.data.redis.core.RedisTemplate; @SpringBootTestclass MultiRedisSourceApplicationTests {

@Autowired RedisTemplate redisTemplate;

@Autowired MultiRedisConnectionFactory multiRedisConnectionFactory;

@Test void contextLoads() {
// 走預設的資料來源
redisTemplate.opsForValue().set("k1","v1");
// 走test資料來源0 庫
multiRedisConnectionFactory.setCurrentRedis("test",0);
// 走test資料來源9 庫
redisTemplate.opsForValue().set("k1","v2");

multiRedisConnectionFactory.setCurrentRedis("test",9);

redisTemplate.opsForValue().set("k1","v2"); } } ```

最後結果

完美,自定義註解加上aop 來動態切換,就是定義一個自定義的註解裡面包含庫名稱和db 的名稱

然後 就是在aop 的前置攔截器上面,或者註解的值, 然後在用MultiRedisConnectionFactory  來設定資料來源和db 。 相關的程式碼已釋出到maven 私服, 使用教程請看 https://ducheng.github.io/dynamicRedisPage/#/