在正常情况下,直接使用springboot提供的配置文件就可以直接配置不同的redis连接,如集群,单体或哨兵。但是,总是有但是。
现在有一个需求,就是用一个redis做缓存,然后用另外一个redis集群做业务数据存储。原有的缓存配置不能改。这就需要手动去配置redis连接了。
这边就直接上代码了,首先展示的是配置类,用来映射自定义的配置文件
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* <p>
* rediskv的配置类。
* <p>
* 通过修改type来适配不同的模式
* <p>
* 在standalone模式下,需要设置 host
* 在sentinel模式下,需要设置 master, nodes
* 在cluster模式下,需要设置 nodes
* <p>
* 剩下的属性是通用的
*/
@Data
@Component
@ConfigurationProperties(prefix = "config.rediskv.client")
public class RedisKVProperties {
private String nodes;
private int maxRedirects = 5;
private String password;
private long timeOut = 5;
private int maxActive = 50;
private int maxIdle = 50;
private boolean metrics = false;
// 类型: cluster, standalone, sentinel
private String type = "standalone";
// 哨兵模式下生效
private String master;
// 单体模式下生效
private String host;
}
相信想象力丰富的已经可以想象出配置文件长什么样子了。
拿到配置的属性后,就要去创建连接,然后定义redisTemplate。自定义的redisTemplate还不能和原有的冲突,所以给设置了bean的name。
这里参考了这个文章。但是每次关闭springboot的时候会报错,似乎是连接池没能正常关闭。
springboot项目中连接2个redis实例
import io.lettuce.core.TimeoutOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import io.lettuce.core.resource.DefaultClientResources;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.MapPropertySource;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.*;
/**
* @description 序列化用的。用的jackson做的(去stack overflow找的)
*/
@Configuration
@ConditionalOnProperty(prefix = "config", name = "kvClient", havingValue = "rediskv")
public class KVRedisConfig {
@Resource
private RedisKVProperties redisKVProperties;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
String type = redisKVProperties.getType();
//连接池配置
switch (type) {
case "cluster":
return clusterConnectionFactory();
case "standalone":
return standaloneConnectionFactory();
case "sentinel":
return sentinelConnectionFactory();
default:
return null;
}
}
@Bean
public DefaultClientResources lettuceClientResources() {
return DefaultClientResources.create();
}
@Bean
@Qualifier("kvRedisTemplate")
public RedisTemplate<Object, Object> kvRedisTemplate() {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
@Qualifier("kvStringRedisTemplate")
public StringRedisTemplate kvStringRedisTemplate() {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
return stringRedisTemplate;
}
/**
* 集群的配置
*/
private RedisConnectionFactory clusterConnectionFactory() {
LettuceClientConfiguration lettuceClientConfiguration = getLettuceClientConfiguration();
Map<String, Object> source = new HashMap<>();
RedisClusterConfiguration redisClusterConfiguration;
source.put("spring.redis.cluster.nodes", redisKVProperties.getNodes());
source.put("spring.redis.cluster.max-redirects", redisKVProperties.getMaxRedirects());
redisClusterConfiguration = new RedisClusterConfiguration(new MapPropertySource("RedisClusterConfiguration", source));
if (!Strings.isEmpty(redisKVProperties.getPassword()))
redisClusterConfiguration.setPassword(redisKVProperties.getPassword());
return new LettuceConnectionFactory(
redisClusterConfiguration, lettuceClientConfiguration
);
}
/**
* 哨兵的配置
*/
private RedisConnectionFactory sentinelConnectionFactory() {
String master = redisKVProperties.getMaster();
String[] nodes = redisKVProperties.getNodes().split(",");
Set<String> hostAndPort = new HashSet<>();
Collections.addAll(hostAndPort, nodes);
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(master, hostAndPort);
if (!Strings.isEmpty(redisKVProperties.getPassword()))
redisSentinelConfiguration.setPassword(redisKVProperties.getPassword());
return new LettuceConnectionFactory(
redisSentinelConfiguration, getLettuceClientConfiguration()
);
}
/**
* 单机的配置
*/
private RedisConnectionFactory standaloneConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisKVProperties.getHost());
String password = redisKVProperties.getPassword();
if (password != null && !"".equals(password))
redisStandaloneConfiguration.setPassword(password);
return new LettuceConnectionFactory(
redisStandaloneConfiguration, getLettuceClientConfiguration()
);
}
/**
* 连接池配置
*/
private LettuceClientConfiguration getLettuceClientConfiguration() {
GenericObjectPoolConfig genericObjectPoolConfig =
new GenericObjectPoolConfig();
genericObjectPoolConfig.setMaxTotal(redisKVProperties.getMaxActive());
genericObjectPoolConfig.setMaxIdle(redisKVProperties.getMaxIdle());
LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder
builder = LettucePoolingClientConfiguration.builder()
.clientOptions(clusterClientOptions())
.clientResources(lettuceClientResources())
.commandTimeout(Duration.ofSeconds(redisKVProperties.getTimeOut()));
builder.poolConfig(genericObjectPoolConfig);
LettuceClientConfiguration lettuceClientConfiguration = builder.build();
return lettuceClientConfiguration;
}
/**
* 拓扑刷新机制。主要是为了防止主机挂掉
*/
private ClusterClientOptions clusterClientOptions() {
ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
.enablePeriodicRefresh(Duration.ofSeconds(30)) //按照周期刷新拓扑
.enableAllAdaptiveRefreshTriggers() //根据事件刷新拓扑
.build();
return ClusterClientOptions.builder()
//redis命令超时时间,超时后才会使用新的拓扑信息重新建立连接
.timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(10)))
.topologyRefreshOptions(topologyRefreshOptions)
.build();
}
}
这头上的注解可以百度。我也是头一次接触。
基本上,上面就是重点内容了。后面就是拿到redisTemplate然后做get与set了
这里提一个小细节。byte[]是用string的形式存进去的。但是byte[]和string来回转换容易出问题。这个问题可以百度。设置成特定的编码后就能保证来回转换不会变了。也可以用其他方式实现,这就是HexUtil没有展示的内容。
关于为啥用jackson。我懒得去引别的依赖。fastjson之前因为换版本除了问题我现在也没搞清楚到底引入哪个fastjson。
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* <p> 如果遇到getObject方法报错,可以试着给对应的实体类增加无参构造函数或@NoArgsConstructor注解
*/
@Slf4j
public class RedisStoreClient {
@Autowired
@Qualifier("kvRedisTemplate")
private RedisTemplate redisTemplate;
@Autowired
@Qualifier("kvStringRedisTemplate")
private StringRedisTemplate stringRedisTemplate;
private String Separator = ",";
@Bean
public ObjectMapper getObjectMapper(){
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
ObjectMapper objectMapper = new ObjectMapper();
builder.configure(objectMapper);
objectMapper = objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
return objectMapper;
}
@Resource
private ObjectMapper objectMapper;
/**
* 存入byte[]对象
* */
public void setBytes(String key, byte[] bytes){
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
setString(key, HexUtil.bytes2HexString(bytes));
}
/**
* 取出byte[]对象
* */
public byte[] getBytes(String key){
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
String value = getString(key);
return HexUtil.hex2Bytes(value);
}
/**
* 取出List<byte[]>
* */
public List<byte[]> getBytesList(Set<String> keys){
if(CollectionUtils.isEmpty(keys)){
throw new RuntimeException("key不能为空!");
}
List<String> multiString = getMultiString(keys);
List<byte[]> result = new ArrayList<>();
multiString.forEach(item -> result.add(HexUtil.hex2Bytes(item)));
return result;
}
/**
* 工具方法,把对象转为json
* 如果要在redis环境下把对象转为string再存入(调用setString方法)
* 一定要使用这个。redisTemplate整个序列化啥的都是基于jackson的。
* */
public String objectToJsonString(Object object){
String res = null;
try {
res = objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
log.error("将对象转换为json数组时发生异常, {}", e.getMessage());
}
return res;
}
/**
* 把string解析为object。是和
* @see #objectToJsonString(Object) 成对儿的方法。
* */
public <T> T jsonStringToObject(String value, Class<T> clazz){
if (value == null || "".equals(value.trim())){
throw new RuntimeException("传入空字符串!");
}
T obj;
try {
obj = (T) clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e.getMessage());
}
try {
obj = objectMapper.readValue(value, clazz);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return obj;
}
/**
* @param key 序列的key。根据key去取出自增序列
* @return incr 返回key对应的序列的值自增后的结果
*/
public Long getSequence(String key) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("序列key不能为空!");
}
RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
return redisAtomicLong.incrementAndGet();
}
/**
* @param key 序列的key。根据key去取出自增序列
* @param step 需要增加的数值
* @return incr 返回key对应的序列加上指定数值后的结果
*/
public Long getSequence(String key, Long step) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("序列key不能为空!");
}
RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
return redisAtomicLong.addAndGet(step);
}
/**
* 根据传入的key把自增序列重置(实际上是直接删除了)
*
* @param key 序列对应的key
*/
public Boolean resetSequence(String key) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("序列key不能为空!");
}
return redisTemplate.delete(key);
}
/**
* 使用scan匹配key后批量查询获取value
* 以下方法需要spring-data-redis版本高于2.7.2才能使用
*/
/*public <T> List<T> getObjectList(String pattern) {
Set<String> keySet = new HashSet<>();
Cursor<String> scan = redisTemplate.scan(ScanOptions.scanOptions().match(pattern).build());
while (scan.hasNext()) {
String next = scan.next();
keySet.add(next);
}
return redisTemplate.opsForValue().multiGet(keySet);
}*/
/**
* set方法
*/
public <T> void setObject(String key, T value) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
redisTemplate.opsForValue().set(key, value);
}
/**
* set方法, 返回bool
*/
public <T> boolean setObjectR(String key, T value) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
redisTemplate.opsForValue().set(key, value);
return true;
}
/**
* get方法
* <p>
* 请 一定不要用 这个方法去取出存入的字符串
* 由于在设置中为redisTemplate(kv)设置了默认用jackson去解析内容,用这个去取字符串会解析失败。
* <p>
* 一定不要用来取存入的字符串。
*
* @see #getString(String)
*/
public <T> T getObject(String key) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
return (T) redisTemplate.opsForValue().get(key);
}
/**
* 为了拯救用string去存对象但是就是想用object去取对象
* 这个方法就是干这个用的。但是我是不推荐使用的
* */
public <T> T getObject(String key, Class<T> clazz){
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
String value = stringRedisTemplate.opsForValue().get(key);
if(value == null){
log.error("未能取得{}的对应的值", key);
}
return jsonStringToObject(value, clazz);
}
/**
* get方法(传入Keys)
*
* @param keySet key的集合
* @return Collection 值的集合
*/
public <T> T getMultiObject(Set<String> keySet) {
if (CollectionUtils.isEmpty(keySet)) {
throw new RuntimeException("key不能为空!");
}
return (T) redisTemplate.opsForValue().multiGet(keySet);
}
/**
* delete方法删除存储的键值对
*/
public Boolean delete(String key) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
return redisTemplate.delete(key);
}
/**
* @param entrySet 键值对
* 通过键值对list批量进行set
*/
public void batchPutByMap(Map<String, Object> entrySet) {
if (CollectionUtils.isEmpty(entrySet)) {
throw new RuntimeException("对象不能为空!");
}
redisTemplate.opsForValue().multiSet(entrySet);
}
/**
* 实际存储到数据库的都是字符串。所以就这样存了。
*/
public void merge(String key, String value) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
stringRedisTemplate.opsForValue().append(key, Separator + value);
}
/**
* merge方法,判断key在操作后是否存在
*/
public Boolean mergeR(String key, String value) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
stringRedisTemplate.opsForValue().append(key, Separator + value);
return true;
}
/**
* string在redis中的get与set方法
*/
public void setString(String key, String value) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
stringRedisTemplate.opsForValue().set(key, value);
}
/**
* string的set方法, 通过再次检查key是否存在返回true或false
*/
public Boolean setStringR(String key, String value) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
stringRedisTemplate.opsForValue().set(key, value);
return true;
}
/**
* 用来取字符串的方法,也建议只用来取字符串。
*/
public String getString(String key) {
if (key == null || "".equals(key.trim())) {
throw new RuntimeException("key不能为空!");
}
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 根据传入的keySet获取多个String
* */
public List<String> getMultiString(Set<String> keys){
if(CollectionUtils.isEmpty(keys)){
throw new RuntimeException("key不能为空!");
}
return stringRedisTemplate.opsForValue().multiGet(keys);
}
/**
* 传入map来批量存入string
* */
public void putMultiString(Map<String, String> stringMap){
if(CollectionUtils.isEmpty(stringMap)){
throw new RuntimeException("集合不能为空!");
}
stringRedisTemplate.opsForValue().multiSet(stringMap);
}
/*
* 对于业务中使用的deleteRange方法,可认为是清空数据库?
* 以下方法需要spring-data-redis版本高于2.7.2才能使用
* */
/*public void flushDB() {
Cursor scan = redisTemplate.scan(ScanOptions.scanOptions().match("*").build());
Set<String> keys = new HashSet<>();
while (scan.hasNext()) {
keys.add((String) scan.next());
}
redisTemplate.delete(keys);
}*/
/**
* 清空指定的pattern,但是网上说这么做会让redis数据库寄了
*/
public void flushDB() {
redisTemplate.getConnectionFactory().getConnection().flushAll();
}
/**
* 根据传入的Set<String> keys 批量删除
*
* @param keys key的集合
*/
public void batchDelete(Set<String> keys) {
if (CollectionUtils.isEmpty(keys)) {
throw new RuntimeException("key不能为空!");
}
redisTemplate.delete(keys);
}
}
代码也没写得多好,也就是上网看了各种文章后汇总出来的东西。就写这么多吧。